Android学习笔记三(JAVA):QuizDemo的初步实现,创建模拟器,视图绑定

本文实现如图1所示的一个简单的QuizDemo。屏幕中间用TextView控件显示题目,下方有TRUE和FALSE两个Button控件,用于回答问题。后台会记录答对的题目的数量。回答问题后会自动跳转到下一题,直到所有的题目都做完。所有的题目都做完后,TRUE和FALSE按钮将变得不能点击,同时屏幕下方会有Toast控件显示全部答对的题目的数量。

图1 QuizDemo运行示意图

1.通用设计原则和MVC软件架构模式

2.虚拟设备和模拟器

3.QuizDemo的初步实现

4.视图绑定


1.先介绍两个软件开发的通用设计原则和MVC软件架构模式:

  • 关注点分离原则 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

不要自我重复。应用程序应避免在多个位置指定与特定概念相关的行为。请将逻辑封装在编程构造中,而不要重复该逻辑。


2.虚拟设备和模拟器

模拟器,就是在电脑上构造一个演示窗口,模拟手机屏幕的App运行效果。可以简单理解为一个在电脑上运行的虚拟安卓手机。如果不想使用模拟器的话,也可以用USB线连接一部安卓手机,直接在手机上查看运行效果。本学习笔记使用模拟器,而不是真实手机。如果在右侧的工具窗口栏没有Device Manager的话,就在Tools->Device Manager,打开页面。在Virtual下点击Create device,创建一个虚拟设备。本学习笔记选择的虚拟设备的版本如图2所示。

图2 新建虚拟设备

下载镜像,完成后会在Device Manager中看到设备。点击Actions中的小三角,就可以启动虚拟设备,创建模拟器了。

3.QuizDemo的初步实现

新建一个名为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文件。

 
图3 新建Question类
Question.java代码清单如下:
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。

 
图4 设置自动生成getter和setter方法

 然后在Question.java文件上右键,选择Generation->Getter and Setter,选中Question类中的两个field,就会自动生成getter方法和setter方法(图5)。

 
 
图5 给Question.java生成getter和setter方法
Question.java完整代码清单如下:
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

posted @ 2022-08-05 17:24  南风小斯  阅读(682)  评论(0编辑  收藏  举报