Android学习笔记三(JAVA):QuizDemo的初步实现,创建模拟器,视图绑定
本文实现如图1所示的一个简单的QuizDemo。屏幕中间用TextView控件显示题目,下方有TRUE和FALSE两个Button控件,用于回答问题。后台会记录答对的题目的数量。回答问题后会自动跳转到下一题,直到所有的题目都做完。所有的题目都做完后,TRUE和FALSE按钮将变得不能点击,同时屏幕下方会有Toast控件显示全部答对的题目的数量。
图1 QuizDemo运行示意图
- 关注点分离原则 Separation of Concerns, SoC
对只与“特定概念、目标”(关注点)相关联的软件组成部分进行“标识、封装和操纵”的能力,即标识、封装和操纵关注点的能力。关注点分离是面向对象的程序设计的核心概念。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用(将针对特定领域问题代码抽象化成较少的程式码,例如将代码封装成function或是class),业务逻辑同特定领域问题的关系通过侧面来封装、维护,这样原本分散在在整个应用程序中的变动就可以很好的管理起来。
MVC架构模式就基于关注点分离原则,将应用对象分为模型(model)、视图(view)和控制器(controller)三部分:
- 模型存储着应用的数据和业务逻辑。模型类通常用来映射与应用相关的一些事物,比如用户、商品、服务器上的图片或者一段电视节目。在QuizDemo中模型就是题目(对象、类)。
- 视图负责通过界面显示内容,以及响应用户的输入,比如触摸动作等。一条简单的经验法则是,只要能够在屏幕上看见的对象,就是视图对象。在QuizDemo中就是activity_main.xml。
- 控制器是视图和模型之间的联系纽带。控制器响应视图触发的各类事件,此外还管理着模型与视图之间的数据流动。在QuizDemo中就是MainActivity.java。
关于MVC模式,其目的主要是将软件用户界面UI和业务逻辑分离以提高代码可扩展性、可复用性、可维护性、灵活性等。我在日常开发中,关于控制器、模型、视图之间的关系也存在很多疑问,为此在网上看了不少介绍文章。结果发现这些文章里面所附着的MVC的架构模式的图片相互之间都有不同之外,这更增加了我的疑惑,同时也让我认为MVC的使用也应该是针对具体项目的。控制器、模型、视图之间的交互到底如何实现也应该针对具体项目。因此,这里只是介绍了网上使用的最多的逻辑划分方式。
- Don't repeat yourself, DRY
不要自我重复。应用程序应避免在多个位置指定与特定概念相关的行为。请将逻辑封装在编程构造中,而不要重复该逻辑。
模拟器,就是在电脑上构造一个演示窗口,模拟手机屏幕的App运行效果。可以简单理解为一个在电脑上运行的虚拟安卓手机。如果不想使用模拟器的话,也可以用USB线连接一部安卓手机,直接在手机上查看运行效果。本学习笔记使用模拟器,而不是真实手机。如果在右侧的工具窗口栏没有Device Manager的话,就在Tools->Device Manager,打开页面。在Virtual下点击Create device,创建一个虚拟设备。本学习笔记选择的虚拟设备的版本如图2所示。
下载镜像,完成后会在Device Manager中看到设备。点击Actions中的小三角,就可以启动虚拟设备,创建模拟器了。
新建一个名为QuizDemo的项目。先在strings.xml文件中添加一些字符串,然后完成QuizDemo的页面布局。
strings.xml代码清单:
<resources> <string name="app_name">QuizDemo</string> <string name="btn_true">True</string> <string name="btn_false">False</string> <string name="test_bj">Beijing is the capital of China.</string> <string name="test_tokyo">Tokyo is the capital of Japan.</string> <string name="test_dc">NewYork is the capital of USA.</string> <string name="test_london">London is the capital of UK.</string> </resources>
layout_main.xml代码清单:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/tx_question" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:layout_margin="20dp" android:textSize="20dp" android:gravity="center"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:gravity="center"> <Button android:id="@+id/btn_true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_true" /> <Button android:id="@+id/btn_false" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_false" android:layout_marginLeft="30dp" /> </LinearLayout> </LinearLayout>
编写好布局文件后,来创建Question模型类。也就是MVC中的model部分。如图3所示,依次右键包名->New->Java Class,在新打开的页面中输入Question,创建Question.java文件。
![](https://img2022.cnblogs.com/blog/1298043/202208/1298043-20220803191955876-936246178.png)
![](https://img2022.cnblogs.com/blog/1298043/202207/1298043-20220728170447323-1908433684.png)
package com.larissa.android.quizdemo; public class Question { private int mQuestionId; private boolean mQuestionAnswer; public Question(int questionId, boolean questionAnswer){ mQuestionId=questionId; mQuestionAnswer=questionAnswer; } }
这里值得注意的是,Question类的构造函数的参数包括int类型的questionId和boolean类型的questionAnswer。也就是说我们在创建一个Question类实例的时候,使用的是题目资源的ID。例如针对
<string name="test_bj">Beijing is the capital of China.</string>
这个题目资源,创建它的Question类实例时,用的是它的ID和它的答案(True)。它的ID是如何获取的呢,对于strings.xml中的字符串资源,用字符串资源的name属性获取其ID。也就是说用R.string.test_bj就可以了。因此创建该题目资源的实例化就是
new Question(R.string.test_bj,true)
然后,我们操作一下,让Android能自动生成类的getter和setter方法。依次点击File->Settings->Editor->Code Style->Java->Code Generation。然后在Naming表单的Field行中,添加m作为前缀,添加s作为Static field的前缀(图4),点击OK。
![](https://img2022.cnblogs.com/blog/1298043/202207/1298043-20220727121440655-1445986356.png)
然后在Question.java文件上右键,选择Generation->Getter and Setter,选中Question类中的两个field,就会自动生成getter方法和setter方法(图5)。
![](https://img2022.cnblogs.com/blog/1298043/202207/1298043-20220727121815358-559717176.png)
![](https://img2022.cnblogs.com/blog/1298043/202207/1298043-20220727121834179-493276021.png)
![](https://img2022.cnblogs.com/blog/1298043/202207/1298043-20220727121911521-412964714.png)
package com.larissa.android.quizdemo; public class Question { private int mQuestionId; private boolean mQuestionAnswer; public int getQuestionId() { return mQuestionId; } public void setQuestionId(int questionId) { mQuestionId = questionId; } public boolean isQuestionAnswer() { return mQuestionAnswer; } public void setQuestionAnswer(boolean questionAnswer) { mQuestionAnswer = questionAnswer; } public Question(int questionId, boolean questionAnswer){ mQuestionId=questionId; mQuestionAnswer=questionAnswer; } }
至此,我们已经完成了MVC中的Model(Question.java)和View(layout_main.xml),下面开始完成Controller(MainActivity.java),让视图能动起来。打开MainActivity.java文件,添加如下所示的代码(tips:如果在*.java文件中显示没有该类型之类的错误,可以按照提示使用alt+enter自动添加引用)(MainActivity.java中的已存在的onCreate(Bundle savedInstanceState)方法用于创建Activity,至于Bundle类型的参数后面会讲解到):
package com.larissa.android.quizdemo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; public class MainActivity extends AppCompatActivity { private TextView mQuestionTextView; // 定义一个TextView部件 private int mCurrentIdx=0; // 当前屏幕所显示的题目的索引
// 初始化Question数组 private Question[] mQuestions=new Question[]{ new Question(R.string.test_bj,true), new Question(R.string.test_dc,false), new Question(R.string.test_london,true), new Question(R.string.test_tokyo,true) }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mQuestionTextView=(TextView) findViewById(R.id.tx_question); // 通过ID查找部件,并绑定给mQuestionTextView int question=mQuestions[mCurrentIdx].getQuestionId(); // 获取当前题目资源的ID mQuestionTextView.setText(question); // 使用资源ID动态给TextView的Text属性赋值 } }
下面给Button部件添加响应。可以使用onClick属性指定MainActivity.java中的方法作为其响应方法。(tips:onClick属性并不是Button控件专有的,是继承自View控件的https://developer.android.google.cn/reference/android/view/View?hl=zh-cn#attr_android:onClick)
先在activity_main.xml中给Button控件添加上onClick属性。activity_main.xml代码清单:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/tx_question" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:layout_margin="20dp" android:textSize="20dp" android:gravity="center"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:gravity="center"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_true" android:onClick="ClickTrue"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_false" android:layout_marginLeft="30dp" android:onClick="ClickFalse"/> </LinearLayout> </LinearLayout>
这里给TRUE Button控件的onClick属性指定的是ClickTrue方法,给FALSE Button控件的onClick属性指定的是ClickFalse方法。然后在MainActivity.java中添加这两个方法的实现。MainActivity.java代码清单:
public class MainActivity extends AppCompatActivity { private TextView mQuestionTextView; private int mCurrentIdx=0; private int count=0; // 定义count,用于记录答对题目的数量 private Question[] mQuestions=new Question[]{ new Question(R.string.test_bj,true), new Question(R.string.test_dc,false), new Question(R.string.test_london,true), new Question(R.string.test_tokyo,true) }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mQuestionTextView=(TextView) findViewById(R.id.tx_question); int question=mQuestions[mCurrentIdx].getQuestionId(); mQuestionTextView.setText(question); } public void ClickTrue(View view){ boolean answer=mQuestions[mCurrentIdx].isQuestionAnswer(); if(answer) count++; mCurrentIdx++; if(mCurrentIdx==mQuestions.length){ String toastText=String.format("%s questions are right.",count); Toast.makeText(MainActivity.this,toastText,Toast.LENGTH_LONG).show(); } else{ int question=mQuestions[mCurrentIdx].getQuestionId(); mQuestionTextView.setText(question); } } public void ClickFalse(View view){ boolean answer=mQuestions[mCurrentIdx].isQuestionAnswer(); if(!answer) count++; mCurrentIdx++; if(mCurrentIdx==mQuestions.length){ String toastText=String.format("%s questions are right.",count); Toast.makeText(MainActivity.this,toastText,Toast.LENGTH_LONG).show(); }else{ int question=mQuestions[mCurrentIdx].getQuestionId(); mQuestionTextView.setText(question); } } }
ClickTrue和ClickFalse方法太相似了,根据DRY原则,我们将其合并为一个ClickMethod方法。那么ClickMethod方法需要能够识别调用它的是TRUE还是FALSE Button。先在activity_main.xml文件中把onClick的属性值改为ClickMethod.
activity_main.xml代码清单:
<Button android:id="@+id/btn_true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_true" android:onClick="ClickMethod"/> <Button android:id="@+id/btn_false" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_false" android:layout_marginLeft="30dp" android:onClick="ClickMethod"/>
另外观察MainActivity.java中的代码,发现用于更新题目的代码也可以抽出来单独写成一个updateQuestion()方法,以增加代码的可读性。修改后的MainActivity.java代码清单如下:
public class MainActivity extends AppCompatActivity { private TextView mQuestionTextView; private int mCurrentIdx=0; private int count=0; private Question[] mQuestions=new Question[]{ new Question(R.string.test_bj,true), new Question(R.string.test_dc,false), new Question(R.string.test_london,true), new Question(R.string.test_tokyo,true) }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mQuestionTextView=(TextView) findViewById(R.id.tx_question); showQuestion(); } public void ClickMethod(View view){ boolean answer=mQuestions[mCurrentIdx].isQuestionAnswer(); if(view.getId()==R.id.btn_true){ if(answer) count++; } else if(view.getId()==R.id.btn_false){ if(!answer) count++; } updateQuestion(); } private void updateQuestion(){ mCurrentIdx++; if(mCurrentIdx==mQuestions.length){ String toastText=String.format("%s questions are right.",count); Toast.makeText(MainActivity.this,toastText,Toast.LENGTH_LONG).show(); }else{ showQuestion(); } } private void showQuestion(){ int question=mQuestions[mCurrentIdx].getQuestionId(); mQuestionTextView.setText(question); } }
至此,点击工具栏中的绿色小三角Run,运行QuizDemo应用,点击TRUE或者FALSE按钮,选择答案,TextView中的题目会自动挑战到下一道题,并且全部题目都回答完后,在屏幕的底部会显示答对题目的数量。
然而,虽然Button部件能够在XML文件中通过onClick属性指定点击方法,但是方法的名称可以随便叫(例如ClickTrue, ClickMethod),这样不利于规范化代码。因此在实际开发中,不推荐使用Button的onClick属性。那怎么办呢?Android应用属于典型的事件驱动类型。事件驱动型应用启动后,即开始等待行为事件的发生,如用户点击某个按钮。应用等待某个特定事件发生,也就是说应用正在监听特定事件。为响应某个事件而创建的对象叫做监听器(listener)。监听器会实现特定事件的监听器接口。Android SDK已经为各种事件内置了很多监听器接口。当前应用需要监听用户的按钮点击事件,因此监听器需实现View.OnClickListener接口。也就是说,可以给按钮对象注册点击监听器。点击监听器实现View.OnClickListener接口,并重写其onClick方法补充点击事件的处理代码,再由Button调用setOnClickListener方法设置监听器对象。
在layout_main.xml文件中,把Button控件的onClick属性删除。然后更改MainActivity.java代码。MainActivity.java代码清单:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView mQuestionTextView; private Button mTrueButton; private Button mFalseButton; private int mCurrentIdx=0; private int count=0; private Question[] mQuestions=new Question[]{ new Question(R.string.test_bj,true), new Question(R.string.test_dc,false), new Question(R.string.test_london,true), new Question(R.string.test_tokyo,true) }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mQuestionTextView=(TextView) findViewById(R.id.tx_question); mTrueButton=(Button) findViewById(R.id.btn_true); mFalseButton=(Button) findViewById(R.id.btn_false); mTrueButton.setOnClickListener(this); mFalseButton.setOnClickListener(this); showQuestion(); } @Override public void onClick(View view){ boolean answer=mQuestions[mCurrentIdx].isQuestionAnswer(); if(view.getId()==R.id.btn_true){ if(answer) count++; } else if(view.getId()==R.id.btn_false){ if(!answer) count++; } updateQuestion(); } private void UpdateQuestion(){ mCurrentIdx++; if(mCurrentIdx==mQuestions.length){ String toastText=String.format("%s questions are right.",count); Toast.makeText(MainActivity.this,toastText,Toast.LENGTH_LONG).show(); }else{ showQuestion(); } } private void ShowQuestion(){ int question=mQuestions[mCurrentIdx].getQuestionId(); mQuestionTextView.setText(question); } }
这里使用setOnClickListener时也可以用匿名方法。写法如下(如果是java初学者,不建议使用匿名方法)(这时可以不用在类名处实现接口,即不用写implements View.OnClickListener,也不用重写独立onClick方法):
mTrueButton.setOnClickListener(view -> { if(mQuestions[mCurrentIdx].isQuestionAnswer()) count++; updateQuestion(); }); mFalseButton.setOnClickListener(view -> { if(!mQuestions[mCurrentIdx].isQuestionAnswer()) count++; updateQuestion(); });
某些情况下,Button需要禁止点击操作。例如大部分APP的登录操作,没有输入手机号时,”登录“按钮都是灰色的,无法点击的。在这种业务场景中,Button先后拥有两种状态,即不可用状态和可用状态。在本例中,当全部的题目都做完时,TRUE和FALSE Button都要变成不可用状态。因此对MainActivity.java中的updateQuestion方法进行如下修改。
private void updateQuestion(){ mCurrentIdx++; if(mCurrentIdx==mQuestions.length){ mTrueButton.setEnabled(false); mFalseButton.setEnabled(false); String toastText=String.format("%s questions are right.",mCount); Toast.makeText(MainActivity.this,toastText,Toast.LENGTH_SHORT).show(); }else{ showQuestion(); } }
至此,QuizDemo已经初步实现了。全部代码见https://gitee.com/larissaLiu/quiz-demo_v1
4.视图绑定 https://developer.android.google.cn/topic/libraries/view-binding?hl=zh-cn
在上述代码中,MainActivity.java中使用activity_main.xml中定义的视图部件,用findViewById()方法,通过id查找到部件,然后再关联该部件的方法。对于Button部件不太多的QuizDemo而言,好像没什么问题。但是如果要实现一个简易的计算器呢,有十多个Button部件的时候,如果也用这种findViewById()的方法,就很不方便了。有没有一种方法能让MainActivity直接使用activity_main.xml中的视图部件呢。答案就是视图绑定(view-binding)。下面用例子来看下视图绑定到底是什么。
视图绑定功能可按模块启用。要在某个模块中启用视图绑定,首先要在build.gradle(Module:*.app) android节点中添加
buildFeatures{
viewBinding true
}
或者在android中添加
viewBinding{
enabled=true
}
系统会自动为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,会转换为Pascal大小写形式并在末尾添加Binding后缀。在本例中,就是会生成ActivityMainBinding类。此类包含从布局属性到布局视图的所有绑定,并知道如何为绑定表达式指定值。
2023.04.03补充内容:
在Android视角下是找不到系统为每个布局文件生成的绑定类的,例如找不到ActivityMainBinding文件。app build之后,切换到Project视角,依次打开app-build-generated-data_binding_base_class_source_out-debug-out-com-XXX-android-project name-databinding就能看到系统生成的绑定类了。通过查看源代码可知,XML布局文件中的每个具有Id的控件在绑定类中都被定义为一个public属性,变量名就是Id值。因此可以直接通过"绑定类实例.变量名"的方式获取到控件的实例。
在MainActivity.java中使用这个类即可。MainActivity.java代码清单如下:
public class MainActivity extends AppCompatActivity { private ActivityMainBinding mActivityMainBinding; private int count=0; private int mCurrentIdx=0; private Question[] mQuestions=new Question[]{ new Question(R.string.test_bj,true), new Question(R.string.test_dc,false), new Question(R.string.test_london,true), new Question(R.string.test_tokyo,true) }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// 将对象绑定到布局的最常用方法是在绑定类上使用静态方法。可以使用绑定类的inflate()方法来扩充视图层次结构并将对象绑定到该层次结构。 mActivityMainBinding=ActivityMainBinding.inflate(getLayoutInflater());
// 每个绑定类有一个getRoot()方法,用于为相应布局文件的根视图提供直接引用。在本例中,getRoot()方法会返回LinearLayout根视图。 View view=mActivityMainBinding.getRoot();
// 关联视图 setContentView(view);
// 使用绑定类的实例引用任何视图部件 mActivityMainBinding.btnTrue.setOnClickListener(view1 -> { if(mQuestions[mCurrentIdx].isQuestionAnswer()) count++; updateQuestion(); }); mActivityMainBinding.btnFalse.setOnClickListener(view1->{ if(!mQuestions[mCurrentIdx].isQuestionAnswer()) count++; updateQuestion(); }); showQuestion(); } private void updateQuestion(){ mCurrentIdx++; if(mCurrentIdx==mQuestions.length){ mActivityMainBinding.btnTrue.setEnabled(false); mActivityMainBinding.btnFalse.setEnabled(false); String toastText=String.format("%s questions are right.",count); Toast.makeText(MainActivity.this,toastText,Toast.LENGTH_LONG).show(); }else{ showQuestion(); } } private void showQuestion(){ int question=mQuestions[mCurrentIdx].getQuestionId(); mActivityMainBinding.txQuestion.setText(question); } }
与使用findViewId相比,视图绑定具有一些显著的优势:
Null安全,由于视图绑定会创建对视图的直接引用,因此不存在因视图ID无效而引发Null指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用@Nullable标记;
类型安全,每个绑定类中的字段均具有与它们在XML文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
2023.04.04补充内容:
视图绑定官方文档给出的使用例子如下:
其中的第一行binding.getName()应该是遗留问题,可能以前使用binding类实例是给每个有Id的控件生成相应的getter方法,然后通过getter方法获取控件实例。但是现在控件都直接定义为public fields了,因此如第二行所示,直接用"绑定类实例.控件Id"的方式获取控件的实例。
视图绑定的源代码点击这里:https://gitee.com/larissaLiu/quiz-demo_-view-binding