结合MVP设计模式和解析Json数据,制作一款“手机号码归属地查询的App小程序(Android)”
说明:实现的原理很简单,有多种设计方式和代码编写风格。本文主要是认识、理解MVP设计模式和Json数据的常见解析框架的使用。
源码:请点击链接访问我的GitHub进行查看准备工作:
- AndroidStudio 开发工具(谷爹的亲儿子)
- 浏览器(进行测试淘宝开放平台返回给我们的Json数据并进行解析)
- 一部真机或者模拟机(能上网的手机)
分析步骤:
1.实现界面
2.获取数据:
(1).本地数据(数据不是最新的)
(2).网络数据(数据是最新的)。【数据提供方:淘宝的开放接口。淘宝API的Web接口】
接口地址:https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=要查询的手机号码 3.解析数据
http框架:Okhttp框架
Json数据处理:Gson,FastJson,自带JsonObject
本例,笔者调用的是自己的手机号。通过解析淘宝开放平台提供的数据接口返回Json数据进行解析
https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13795950539 4.展现数据
MVP通知界面-》展示数据
Json解析框架介绍:
JSONObject:安卓自带的Json解析框架,采用对象和数组的2种方式解析.不能直接把JSon转化为java对象
Gson:Java对象和Json可以相互直接转换
FastJson:阿里提供的库,用法和Gson差不多,小巧,解析快
关于MVP和MVC的简要说明:
层次分明的代码:
低耦合:业务层-数据层-展示层。(要分离)
1.MVC模式
2.衍生MVP模式(业务和界面进行分离)
Activity充当了 MVP中的View 的“角色”,业务逻辑在Presenter进行实现
View与Presenter进行解耦:采用接口交互。(即笔者在此把View的主要操作抽象为接口,Presenter通过接口与View进行交互,所以Presenter就不必依赖于具体的View对象)
MVC:
MVP:
MVP的好处:
MVP的使用:
MVC和MVP的结构对比:
注:实战中代码的具体说明已经注释到相应代码段中,如有需要,请自行在GitHub中搜索我的库进行查看理解
编写程序:AndroidStudio 3.3
- 资源文件:
strings.xml:
<resources>
<string name="app_name">手机号码归属地查询</string>
<string name="query_phone">手机号码查询:</string>
<string name="input_phone">请输入手机号码查询</string>
<string name="query">查询</string>
<string name="result_query">查询结果:</string>
<string name="phone">手机号:未知</string>
<string name="province">省份:未知</string>
<string name="type">运营商:未知</string>
<string name="belongtype">归属运营商:未知</string>
</resources>
colors.xml:
颜色不重要,自定义即可
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
<!--自定义颜色-->
<!--标题颜色-->
<color name="title_text_color">#F44336</color>
<!--输入框提示文字颜色-->
<color name="hint_color">#F44336</color>
<!--按钮背景-->
<color name="button_backgrond_color">#54673AB7</color>
<!--查询结果标题文字颜色-->
<color name="search_title_color">#3F51B5</color>
<!--透明-->
<color name="BarColor">#00FFFFFF</color>
</resources>
styles.xml:
主要是在默认的styles文件中,去掉了ActionBar,并且底部栏改成了白色
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:navigationBarColor" tools:targetApi="lollipop">@color/BarColor</item>
</style>
</resources>
- 界面布局:
activity_main.xml:(文末有界面图片展示,有需要的话请自行复制)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
android:background="@drawable/background"
tools:context=".MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#673AB7"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
</android.support.v7.widget.Toolbar>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:textColor="@color/title_text_color"
android:text="@string/query_phone"
android:textSize="20sp"/>
<EditText
android:id="@+id/input_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:singleLine="true"
android:maxLength="11"
android:inputType="phone"
android:textColor="#FF5722"
android:textColorHint="@color/hint_color"
android:hint="@string/input_phone"
android:textSize="20sp"/>
<Button
android:id="@+id/btn_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:layout_marginTop="10dp"
android:layout_gravity="center_horizontal"
android:background="@color/button_backgrond_color"
android:textColor="#ffff"
android:text="@string/query"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_gravity="center_horizontal"
android:textColor="@color/hint_color"
android:text="@string/result_query"
android:textSize="18sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginTop="10dp"
android:layout_marginRight="30dp"
android:background="@color/button_backgrond_color"
android:orientation="vertical">
<TextView
android:id="@+id/result_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/button_backgrond_color"
android:paddingLeft="40dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:text="@string/phone"
android:textColor="#FF5722"
android:textSize="20sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#ffff" />
<TextView
android:id="@+id/result_province"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/button_backgrond_color"
android:paddingLeft="40dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:text="@string/province"
android:textColor="#FF5722"
android:textSize="20sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#ffff" />
<TextView
android:id="@+id/result_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/button_backgrond_color"
android:paddingLeft="40dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:text="@string/type"
android:textColor="#FF5722"
android:textSize="20sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#ffff" />
<TextView
android:id="@+id/result_carrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/button_backgrond_color"
android:paddingLeft="40dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:text="@string/belongtype"
android:textColor="#FF5722"
android:textSize="20sp" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
- MVP目录结构大致理解:
1.先看Model层次:
在笔者自己的理解中,Model无非就是一个模型,我们可以理解为一个“模特(穿没穿衣服自己想象)”。然后就是这个Class的get()、set()方法,有了这样的条件,就组成了“人”的特点。所以说,Model也就是类的实体层,用于保存实例状态(保存“模特穿衣服的状态”)。
2.再看View层次:
其实说白了,View层就是我们的Activity.对于MVC设计模式,Activity充当着View和一部分Controller的角色。导致Activity的代码很臃肿,且不易维护,看起来蛋疼,没有真正做到一目了然的效果。所以,MVP就是把View和Controller分离开,便于项目的维护和升级。
3.大哥大:Presenter(中文意思:委托者)也称为业务逻辑处理实现。【说白了,复杂的东西交给它来做】
Presenter是Model和View之间的桥梁。
看下图:
Model和View没有直接联系,那么他们是怎么进行交互的呢?
答案当然是MVP的核心思想:把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model
这样,Activity的工作就简单了,只用来响应生命周期。 - MVP设计模式主要代码:
(1).Model->Phone(实体):
package com.example.yinlei.phoneattributionquery.model;
/**
* Model。可理解为没有数据的模型,存储我们的手机号码
* 代表手机号这个实体。
* 有哪些字段需要我们通过预先对数据源接口返回的Json数据进行观察
* Example:淘宝返回给我的json数据为
* _GetZoneResult_={
* mts:'1379595',
* province:'四川',
* catName:'中国移动',
* telString:'13795950539',
* areaVid:'30508',
* ispVid:'3236139'
* carrier:'四川移动'
* }
*/
public class Phone {
String telString;//手机号
String province;//省份
String catName;//运营商
String carrier;//手机运营商
public String getTelString() {
return telString;
}
public void setTelString(String telString) {
this.telString = telString;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCatName() {
return catName;
}
public void setCatName(String catName) {
this.catName = catName;
}
public String getCarrier() {
return carrier;
}
public void setCarrier(String carrier) {
this.carrier = carrier;
}
}
上面的代码的原理笔者在上面已经阐述过了,就不再啰嗦了,请自行查看理解。
(2).View ->MainActivity:
因为MVP的Presenter和View进行交互是通过接口,所以这里实现了Presenter的impl包下的MvpMainView接口来传递数据【剩下就是接口回调的事了】
在这里简单实例化一下控件,点击监听OnClick()方法请先忽略,等下了解了Presenter后返回来处理。
package com.example.yinlei.phoneattributionquery;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.example.yinlei.phoneattributionquery.model.Phone;
import com.example.yinlei.phoneattributionquery.mvp.MvpMainView;
import com.example.yinlei.phoneattributionquery.mvp.impl.MainPresenter;
public class MainActivity extends AppCompatActivity implements View.OnClickListener, MvpMainView {
EditText input_phone;
Button btn_search;
TextView result_phone;
TextView result_province;
TextView result_type;
TextView result_carrier;
MainPresenter mainPresenter;
ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**设置Toolbar*/
Toolbar toolbar=findViewById(R.id.toolbar);//此处注意导入android.support.v7.widget.Toolbar。此包对应于android.support.v7.app.AppCompatActivity
setSupportActionBar(toolbar);
initView();//初始化控件
btn_search.setOnClickListener(this);
mainPresenter=new MainPresenter(this);
mainPresenter.attach(this);
}
/**
* 初始化控件
*/
public void initView(){
input_phone=findViewById(R.id.input_phone);
btn_search=findViewById(R.id.btn_search);
result_phone=findViewById(R.id.result_phone);
result_province=findViewById(R.id.result_province);
result_type=findViewById(R.id.result_type);
result_carrier=findViewById(R.id.result_carrier);
progressBar=findViewById(R.id.progress);
}
@Override
public void onClick(View v) {
// Toast.makeText(this, "点击了查询按钮", Toast.LENGTH_SHORT).show();
mainPresenter.searchPhoneInfo(input_phone.getText().toString());//调用Presenter中的查询手机号码业务
}
/**以下是MvpMainView接口的方法*/
@Override
public void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
@Override
public void updateView() {
Phone phone= mainPresenter.getPhoneInfo();
result_phone.setText("手机号码:"+phone.getTelString());
result_province.setText("省份:"+phone.getProvince());
result_type.setText("运营商:"+phone.getCatName());
result_carrier.setText("归属运营商:"+phone.getCarrier());
}
@Override
public void showLoading() {
if (progressBar==null) {
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
public void hidenLoading() {
if (progressBar!=null){
progressBar.setVisibility(View.GONE);
}
}
@Override
protected void onDestroy() {
mainPresenter.onDestroy();
super.onDestroy();
}
}
(3).Presenter:委托者
首先,把业务逻辑抽象成接口:
这里的MvpLoadingView接口是父类接口,把常用的方法提取到了父类。
MvpLoadingView:
package com.example.yinlei.phoneattributionquery.mvp;
/**
* MvpMainView接口的父类
* 主要是方便MvpMainView对父接口的复用
*/
public interface MvpLodingView {
void showLoading();
void hidenLoading();
}
MvpMainView:
package com.example.yinlei.phoneattributionquery.mvp;
/**
* Presenter通过View的操作都是通过这个接口来进行处理业务
*/
public interface MvpMainView extends MvpLodingView{
void showToast(String msg);
void updateView();
}
再创建Presenter:
其中,BasePresenter只是把常用的方法写了去,比如生命周期
package com.example.yinlei.phoneattributionquery.mvp.impl;
import android.content.Context;
/**
* 把Activity中常用的方法写在基类里面
*/
public class BasePresenter {
Context mContext;
public void attach(Context context){
mContext=context;
}
public void onPause(){}
public void onResume(){}
public void onDestroy(){
mContext=null;//释放掉mContext.避免Activity在释放的时候,Presenter却还在使用Context而造成Activity释放不了引起内存泄漏
}
}
注:这里的Context与MainActivity进行绑定,建立关联,避免了内存泄漏
然后来看看MainPresenter中主要做了什么事情:
package com.example.yinlei.phoneattributionquery.mvp.impl;
import android.widget.Toast;
import com.example.yinlei.phoneattributionquery.Until.HttpUntil;
import com.example.yinlei.phoneattributionquery.model.Phone;
import com.example.yinlei.phoneattributionquery.mvp.MvpMainView;
import com.google.gson.Gson;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* 处理MainActivity的逻辑
* Presenter靠本类进行接口交互。
*/
public class MainPresenter extends BasePresenter{
// 示例Url:https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13795950539
String mUrl="https://tcc.taobao.com/cc/json/mobile_tel_segment.htm";
MvpMainView mvpMainView;//保存传递过来的MvpMainView接口数据
Phone mPhone;//保存下方接口解析后的Phone实例
public MainPresenter(MvpMainView mainView){
mvpMainView=mainView;
}
public Phone getPhoneInfo(){//返回解析后并保存后的Phone实例
return mPhone;
}
/**
* 查询手机号
* @param phone
*/
public void searchPhoneInfo(String phone){
if (phone.length()!=11){
mvpMainView.showToast("请输入正确的手机号码!");
return;
}
mvpMainView.showLoading();
//Http请求的处理逻辑
sendHttp(phone);
}
/**
* Http请求的处理逻辑
* @param phone
*/
private void sendHttp(String phone){
//构造Map,然后作为形参传入工具类中
final Map<String,String>map=new HashMap<String, String>();
map.put("tel",phone);
//实例化网络请求工具类
HttpUntil httpUntil=new HttpUntil(new HttpUntil.HttpResponse() {//构造方法需要我们传入接口(实例接口变量)
@Override
public void onSuccess(Object object) {//返回
String json=object.toString();//返回给我们的数据:包含json数据
int index=json.indexOf("{");
json=json.substring(index);
//上面的操作是提取数据中的json,去掉多余的数据
//mPhone=parseJson(json);//自定义函数:采用安卓自带的JsonObject框架
// mPhone=parseJsonWithGson(json);
mPhone=parseJsonWithFastJson(json);
mvpMainView.hidenLoading();
mvpMainView.updateView();
}
@Override
public void onFail(String error) {
//通知View显示失败信息,关闭Loading界面
mvpMainView.showToast(error);//接口回调:接口变量中的showToast方法有了数据。所以实现mvpMainView接口,就可以接口回调
mvpMainView.hidenLoading();
}
});
httpUntil.sendGetHttp(mUrl,map);//调用工具类中定义的通过Get方式请求的方法
}
/**
* 解析淘宝API返回给我们的Json数据
* 方法1:通过JSONobjectn框架解析
* @param json
* @return Phone
*/
private Phone parseJson(String json){
Phone phone=new Phone();
try {
JSONObject jsonObject=new JSONObject(json);
String informationOfJson=jsonObject.getString("telString");
phone.setTelString(informationOfJson);
informationOfJson=jsonObject.getString("province");
phone.setProvince(informationOfJson);
informationOfJson=jsonObject.getString("catName");
phone.setCatName(informationOfJson);
informationOfJson=jsonObject.getString("carrier");
phone.setCarrier(informationOfJson);
} catch (JSONException e) {
e.printStackTrace();
}
return phone;
}
/**
* 方法2:解析Json通过GSON框架
* @param json
* @return Phone
*/
private Phone parseJsonWithGson(String json){
Gson gson=new Gson();
// String myJson=gson.toJson(json);//处理空格
Phone phone=gson.fromJson(json,Phone.class);//直接将Json转化为JAVA对象
return phone;
}
/**
* 方法3:解析Json通过FastJson框架(阿里)
* @param json
* @return Phone
*/
private Phone parseJsonWithFastJson(String json){
Phone phone= com.alibaba.fastjson.JSONObject.parseObject(json,Phone.class);
return phone;
}
}
本类主要是对于数据进行处理,比如网络请求,请求后再对返回的JSon数据进行解析。注意:这里的String phone是输入框获取到的用户输入,那么可想而知,我们应该对searchPhoneInfo()方法在MainActivity中的按钮监听事件中进行调用。
对于网络请求:为了便于维护,写在了工具包Until中,在本类直接 HttpUntil httpUntil=new HttpUntil(new HttpUntil.HttpResponse() {}就可以调用网络请求:
工具类下的HttpUntil类发送网络请求:(get/post方式都写上了,实际上本例只需要Post方式请求网络)
package com.example.yinlei.phoneattributionquery.Until;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* 工具类:进行Http请求
*/
public class HttpUntil {
String mUrl;
Map<String,String>mParam;
HttpResponse mHttpResponse;//接口变量,保存接收到的接口数据
private final OkHttpClient client=new OkHttpClient();
Handler mHandler=new Handler(Looper.getMainLooper());//使用主线程(即Ui线程).用Handler处理线程之间的传递问题,即主线程与子线程之间的交互问题解决
/**回调接口,拿到请求返回的结果*/
public interface HttpResponse{
void onSuccess(Object object);
void onFail(String error);
}
public HttpUntil(HttpResponse response) {//构造函数:进行接口回调
mHttpResponse=response;
}
/**Post请求方式提交表单*/
public void sendPostHttp(String url, Map<String,String>param){
sendHttp(url,param,true);
}
/**Get请求方式提交表单*/
public void sendGetHttp(String url,Map<String,String>param){
sendHttp(url,param,false);
}
/**通过上面的2个方法中任选一个方法,传入定义方法的参数值isPost*/
private void sendHttp(String url,Map<String,String>param,boolean isPost){//Post请求方式
mUrl=url;
mParam=param;
//编写Http请求逻辑
request(isPost);//自定义请求方法
}
/**
* 自定义请求方法:使用Okhttp网络请求框架
*
*/
private void request(boolean isPost){
//request请求创建
Request request=createRequest(isPost);//构建OkHttp中的Request实例
//创建请求队列Call
client.newCall(request).enqueue(new Callback() {//OkHttp中的匿名回调类(即接口回调)
@Override
public void onFailure(Call call, IOException e) {
if (mHttpResponse!=null){//回调是否有赋值,有回调,就把回调结果返回给接口HttpResponse
//考虑一个问题:Call接口回调是运行在子线程中,如果直接调用回调方法,在UI线程中使用就不方便。所以在UI线程中去调用回调接口,需要全局的Handler进行处理
mHandler.post(new Runnable() {
@Override
public void run() {
mHttpResponse.onFail("请求错误!");
}
});
}
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
if (mHttpResponse==null)return;
//HttpResponse的接口有被调用
mHandler.post(new Runnable() {
@Override
public void run() {
if (!response.isSuccessful()){//请求失败
mHttpResponse.onFail("请求失败:"+response);
}else {//请求成功
try {
mHttpResponse.onSuccess(response.body().string());
} catch (IOException e) {
e.printStackTrace();
mHttpResponse.onFail("结果转换失败!");
}
}
}
});
}
});
}
/**
*自定义函数:封装了一下Okhttp中,构造一个Request对象实例
* @param isPost
* @return
*/
private Request createRequest(boolean isPost){
Request request;
if (isPost){//是Post请求:需要提交请求参数
//构建表单
MultipartBody.Builder requestBodyBuilder=new MultipartBody.Builder();
requestBodyBuilder.setType(MultipartBody.FORM);//设置类型
//遍历Map请求参数:迭代器Iterator
Iterator<Map.Entry<String,String>> iterator=mParam.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,String>entry=iterator.next();
requestBodyBuilder.addFormDataPart(entry.getKey(),entry.getValue());
}
request=new Request.Builder().url(mUrl)
.post(requestBodyBuilder.build()).build();//构造Okhttp3的请求Request实例【Post方式】
}else {//是Get请求:地址和参数进行拼接
String urlStr=mUrl+"?"+MapParamToString(mParam);
request=new Request.Builder().url(urlStr).build();//构造Okhttp3的请求Request实例【Get方式】
}
return request;//返回经过逻辑处理完成的Request对象
}
/**
* 自定义方法:Map中的值按照url的格式进行拼接并返回它,然后再和mUrl进行完整的拼接
* @param param
* @return
*/
private String MapParamToString(Map<String,String>param){
StringBuilder stringBuilder=new StringBuilder();//字符串构造器
/**遍历Map*/
Iterator<Map.Entry<String,String>>iterator=param.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,String>entry=iterator.next();
//把Map参数按http的Get请求方式进行拼接
stringBuilder.append(entry.getKey()+"="+entry.getValue()+"&");
}
//求掉最后一个"&"符号
String str=stringBuilder.toString().substring(0,stringBuilder.length()-1);//subString()方法是提取一段String【java的String基础和数据结构中的串的知识点】
return str;
}
}
说明:本类方法还定义了一个接口,该类的内部定义的接口HttpResponse用于保存Okhttp回调时候返回的信息和 返回的Json数据。然后在MainPresenter中进行调用工具类的构造方法的时候就通过匿名实现接口HttpResponse中的方法。此时,实现的方法的参数是有数据的,且是工具类中接口已被赋有数据。即MainPresenter实现HttpResponse接口的方法,它的形参是已经被赋值后的。(理解接口回调)
逻辑就这样了,最后关于MainPresenter主要的业务是对Json数据进行解析,并转化成Phone(Model层)实例来保存解析后的带有数据的Model.(即现在是穿了衣服的模特啦!然后在View里面对界面进行展示数据通过Model的get和set方法)。结果展示: