1回答

0收藏

译:使用kotlin来开发一个现代Android项目(一)

Android Android 21448 人阅读 | 1 人回复 | 2020-09-14

市面上很难找到一个能够覆盖所有Android新事物的项目,所以我要自己写一个,它使用一下几项技术:
  • Android Studio
  • Kotlin
  • 构建变种版本
  • 约束布局
  • 数据绑定
  • MVVM+repository+Android Manager架构模式
  • RxJava
  • Dagger依赖注入
  • Retrofit+RxJava2实现网络请求
  • RooM+RxJava实现存储
在以上几项技术中,Android studio并不算稀奇,因为目前绝大多数Android开发者都在使用Android Studio来做开发工具,所以还是要学习下面9种技术。其中,kotlin语言我已经接触过了,它的写法更加简单,另外可以非常简单的解决异步线程的问题;构建变种版本就是为同一个应用创建不同的版本;约束布局,一种新的布局,用法上类似相对布局;数据绑定,让实现MVVM非常的简单,剩下的RxJava等技术就不做概念上的介绍了,这个定义起来实在是太难了。

0、Android Studio
下载安装Android Studio的过程就直接省略掉了,直接创建一个新的项目,进入到select a Project Template,选择Empty Activity,点击next,名字和包名自己写一下,Language 需要选择Kotlin,finish。

接下面我们只需要静静的等待Android Studio为我们构建完成,然后我们就可以在实体机或者虚拟机内运行这个app。当然,如果有报错的话可以看看右下方有没有提示信息,比如要你安装kotlin 1.4,安装即可。

1、Kotlin

我们可以看到这个MainActivity的后缀是kt,这说明它是一个kotlin文件,MainActivity : AppCompatActivity() 表示这个MainActivity继承自AppCompatActivity。

另外,在kotlin种,所有的方法都需要有fun关键字,并且不可以使用@override注解,如果需要复写方法的话可以直接使用override关键字。


savedInstanceState: Bundle?  这个?代表什么呢,它代表了这个Bundle可以为空,即NULL。kotlin是一门null安全语言。
举个例子:
  1. var a : String
复制代码
在编译器中写入这段代码之后,会得到一个编译错误,因为a的值必须被初始化,就像下面这样:
  1. var a : String = "Init value"
复制代码
如果把null赋值给a的话,也会得到一个编译错误:
  1. a = null
复制代码
想要使a的值可以为空,可以这样写:
  1. var a : String?
复制代码
通过这样的写法,来帮助我们避免空指针,例如我们有一个可以为空并且已经为空的nameTextView,需要设置它的值,那么我们使用下面的代码赋值将会产生空指针异常:
  1. nameTextView.setText("Hello World")
复制代码
但是如果使用Kotlin,它将不允许我们这样做,我们必须要使用?或者!!,如果我们使用?
  1. nameTextView?.setText("Hello World")
复制代码
只有当nameTextView不为空时,才会执行赋值操作,这也就避免了空指针异常。不过就和所有的其他事物一样,当我们想要强制使用的时候,同样可以通过!!来使用,例如:
  1. nameTextView!!.setText("Hello World")
复制代码
这时,就很容易发生空指针异常了。

2. 构建变体
一般在开发过程中,你有不同的环境,最常见的就是测试环境和生产环境,这些环境的URL地址、图标、名字、API等都有可能不同,通常在每个项目开始的时候,都会有一下几点:
  • finalProduction:上传到Google play商店
  • demoProduction: 这是一个带有新功能的版本,但是Google商店中还没有更新,用户可以安装这个版本,然后可以测试新功能和提供反馈。
  • demoTesting:和demoProduction一样,不过它的地址为测试地址。
  • mock:对开发和设计都很有用,可以提供假数据,这也就可以在api没有准备好的时候开始开发。一旦API准备好之后,我们就可以把开发转移到demoTesting环境中。
在应用中,我们将会拥有这些变体。他们将提供不同的包名和名称。gradle 3.0.0中有一个新的api:flavorDimension,它允许你混合不同的产品,所以你可以混合demo和minApi23。在我们的app中,我们将仅仅使用默认的flavorDimension,去生成gradle并将此代码插入android{}:
  1. flavorDimensions "default"
  2.    
  3. productFlavors {

  4.     finalProduction {
  5.         dimension "default"
  6.         applicationId "me.mladenrakonjac.modernandroidapp"
  7.         resValue "string", "app_name", "Modern App"
  8.     }

  9.     demoProduction {
  10.         dimension "default"
  11.         applicationId "me.mladenrakonjac.modernandroidapp.demoproduction"
  12.         resValue "string", "app_name", "Modern App Demo P"
  13.     }

  14.     demoTesting {
  15.         dimension "default"
  16.         applicationId "me.mladenrakonjac.modernandroidapp.demotesting"
  17.         resValue "string", "app_name", "Modern App Demo T"
  18.     }


  19.     mock {
  20.         dimension "default"
  21.         applicationId "me.mladenrakonjac.modernandroidapp.mock"
  22.         resValue "string", "app_name", "Modern App Mock"
  23.     }
  24. }
复制代码
打开strings.xml并删除app_name,这也才不会发生资源冲突,然后点击Sync Now(右上角大象),在屏幕的左下角,可以点击构建变体,然后就可以看到4个不同的构建变体,其中每个都有两种构建类型:Debug和Release,切换到demoProduction构建变体并运行它,然后再切换到另外一个并运行,就可以看到两个名称不同的应用程序。

构建变体

构建变体



3. 约束布局
如果你打开activity_main.xml你可以看到根布局时ConstraintLayout,如果你开发过ios应用程序(我想不是很多吧),你可能知道AutoLayout,ConstraintLayout和它非常的相似,他们甚至使用了相同的Cassowary算法。
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"
  4.     xmlns:tools="http://schemas.android.com/tools"
  5.     android:layout_width="match_parent"
  6.     android:layout_height="match_parent"
  7.     tools:context="me.mladenrakonjac.modernandroidapp.MainActivity">

  8.     <TextView
  9.         android:layout_width="wrap_content"
  10.         android:layout_height="wrap_content"
  11.         android:text="Hello World!"
  12.         app:layout_constraintBottom_toBottomOf="parent"
  13.         app:layout_constraintLeft_toLeftOf="parent"
  14.         app:layout_constraintRight_toRightOf="parent"
  15.         app:layout_constraintTop_toTopOf="parent" />

  16. </android.support.constraint.ConstraintLayout>

复制代码
Constraints帮助我们去描述view之间的关系,对于每一个view来说, 你应该有4个约束,一边一个。在这种情况下,我们的视图被父布局的每一边所约束。
如果你在设计选项卡中向上移动一点Hello World这个TextView,在文本选项卡中将出现一行新的代码:
  1. app:layout_constraintVertical_bias="0.28"
复制代码
1_KIuQa8AurG-L30M4nscu0g.png

设计选项卡和文本选项卡时同步的(就是Code、Split、Design那些),修改时会同步修改,垂直偏差描述了视图对其约束的垂直趋势,如果希望视图垂直居中,则应使用:
  1. app:layout_constraintVertical_bias="0.5"
复制代码
这个代码原文应该写错了,因为要居中,所以值应该是0.5。
1_fTnFgSc52wAz3LBkLkMQZg.png


要获得这也的布局,xml应该这也写:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"
  4.     xmlns:tools="http://schemas.android.com/tools"
  5.     android:layout_width="match_parent"
  6.     android:layout_height="match_parent"
  7.     tools:context="com.android.newkotlin.MainActivity">

  8.     <TextView
  9.         android:id="@+id/repository_name"
  10.         android:layout_width="wrap_content"
  11.         android:layout_height="wrap_content"
  12.         android:layout_marginEnd="16dp"
  13.         android:layout_marginStart="16dp"
  14.         android:textSize="20sp"
  15.         app:layout_constraintBottom_toBottomOf="parent"
  16.         app:layout_constraintHorizontal_bias="0.0"
  17.         app:layout_constraintLeft_toLeftOf="parent"
  18.         app:layout_constraintRight_toRightOf="parent"
  19.         app:layout_constraintTop_toTopOf="parent"
  20.         app:layout_constraintVertical_bias="0.083"
  21.         tools:text="Modern Android app" />

  22.     <TextView
  23.         android:id="@+id/repository_has_issues"
  24.         android:layout_width="wrap_content"
  25.         android:layout_height="wrap_content"
  26.         android:layout_marginEnd="16dp"
  27.         android:layout_marginStart="16dp"
  28.         android:layout_marginTop="8dp"
  29.         android:text="@string/has_issues"
  30.         android:textStyle="bold"
  31.         app:layout_constraintBottom_toBottomOf="@+id/repository_name"
  32.         app:layout_constraintEnd_toEndOf="parent"
  33.         app:layout_constraintHorizontal_bias="1.0"
  34.         app:layout_constraintStart_toEndOf="@+id/repository_name"
  35.         app:layout_constraintTop_toTopOf="@+id/repository_name"
  36.         app:layout_constraintVertical_bias="1.0" />

  37.     <TextView
  38.         android:id="@+id/repository_owner"
  39.         android:layout_width="0dp"
  40.         android:layout_height="wrap_content"
  41.         android:layout_marginBottom="8dp"
  42.         android:layout_marginEnd="16dp"
  43.         android:layout_marginStart="16dp"
  44.         android:layout_marginTop="8dp"
  45.         app:layout_constraintBottom_toBottomOf="parent"
  46.         app:layout_constraintEnd_toEndOf="parent"
  47.         app:layout_constraintStart_toStartOf="parent"
  48.         app:layout_constraintTop_toBottomOf="@+id/repository_name"
  49.         app:layout_constraintVertical_bias="0.0"
  50.         tools:text="Mladen Rakonjac" />

  51.     <TextView
  52.         android:id="@+id/number_of_starts"
  53.         android:layout_width="wrap_content"
  54.         android:layout_height="wrap_content"
  55.         android:layout_marginBottom="8dp"
  56.         android:layout_marginEnd="16dp"
  57.         android:layout_marginStart="16dp"
  58.         android:layout_marginTop="8dp"
  59.         app:layout_constraintBottom_toBottomOf="parent"
  60.         app:layout_constraintEnd_toEndOf="parent"
  61.         app:layout_constraintHorizontal_bias="1"
  62.         app:layout_constraintStart_toStartOf="parent"
  63.         app:layout_constraintTop_toBottomOf="@+id/repository_owner"
  64.         app:layout_constraintVertical_bias="0.0"
  65.         tools:text="0 stars" />

  66. </android.support.constraint.ConstraintLayout>
复制代码
不要被tools:text困惑,它仅仅是帮助我们更好的预览布局。

我们可以注意到我们的布局都是扁平的,没有嵌套布局。你应该尽可能的减少使用嵌套布局因为这会影响性能。另外,约束布局可以在不同大小的屏幕下工作的很好。

4. 数据绑定 Data binding library当我听到数据绑定之后的第一件事就是问自己:Butterknife已经很好了,加上我使用一个插件帮助我去从xml中获取视图。我为什么要改变去使用Data binding library呢?当我对Data binding有了更多的了解之后,我对它的感觉就像是第一次见到ButterKnife一样,真香!

Butterknife能帮我们做什么?

使用Butterknife之后,我们就可以不在使用findViewById了,所以,如果你有5个视图,没有使用Butterknife,你需要5+5行代码来绑定你的视图,而用了Butterknife之后,你只需要5行,就这样。

Butterknife有什么坏的地方?

Butterknife仍然无法解决代码维护的问题。当我使用ButterKnife时,我经常发现自己得到一个运行时异常,因为我删除了xml中的视图,而且没有删除activity/fragment中的绑定代码。另外,如果要在xml中添加视图,必须再次进行绑定。

数据绑定库怎么样?

它有很多的好处,使用数据绑定库,你可以仅用一行代码绑定视图!让我来告诉你它是怎么工作的,让我们先将数据绑定库添加到我们的项目中:
  1. // at the top of file
  2. apply plugin: 'kotlin-kapt'


  3. android {
  4.     //other things that we already used
  5.     dataBinding.enabled = true
  6. }
  7. dependencies {
  8.     //other dependencies that we used
  9.     kapt "com.android.databinding:compiler:4.0.0-beta1"
  10. }
复制代码
注意,Data Binding 编译器的版本与项目 build.gradle 文件中的 gradle 版本相同:
  1. classpath 'com.android.tools.build:gradle:4.0.0-beta1'
复制代码
点击 Sync Now,转到 activity _ main.xml class,用 Layout 标签包装 Constraint Layout:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"
  4.     xmlns:tools="http://schemas.android.com/tools">

  5.     <androidx.constraintlayout.widget.ConstraintLayout
  6.         android:layout_width="match_parent"
  7.         android:layout_height="match_parent"
  8.         tools:context="me.mladenrakonjac.modernandroidapp.MainActivity">

  9.         <TextView
  10.             android:id="@+id/repository_name"
  11.             android:layout_width="wrap_content"
  12.             android:layout_height="wrap_content"
  13.             android:layout_marginEnd="16dp"
  14.             android:layout_marginStart="16dp"
  15.             android:textSize="20sp"
  16.             app:layout_constraintBottom_toBottomOf="parent"
  17.             app:layout_constraintHorizontal_bias="0.0"
  18.             app:layout_constraintLeft_toLeftOf="parent"
  19.             app:layout_constraintRight_toRightOf="parent"
  20.             app:layout_constraintTop_toTopOf="parent"
  21.             app:layout_constraintVertical_bias="0.083"
  22.             tools:text="Modern Android app" />

  23.         <TextView
  24.             android:id="@+id/repository_has_issues"
  25.             android:layout_width="wrap_content"
  26.             android:layout_height="wrap_content"
  27.             android:layout_marginEnd="16dp"
  28.             android:layout_marginStart="16dp"
  29.             android:layout_marginTop="8dp"
  30.             android:text="issues"
  31.             android:textStyle="bold"
  32.             app:layout_constraintBottom_toBottomOf="@+id/repository_name"
  33.             app:layout_constraintEnd_toEndOf="parent"
  34.             app:layout_constraintHorizontal_bias="1.0"
  35.             app:layout_constraintStart_toEndOf="@+id/repository_name"
  36.             app:layout_constraintTop_toTopOf="@+id/repository_name"
  37.             app:layout_constraintVertical_bias="1.0" />

  38.         <TextView
  39.             android:id="@+id/repository_owner"
  40.             android:layout_width="0dp"
  41.             android:layout_height="wrap_content"
  42.             android:layout_marginBottom="8dp"
  43.             android:layout_marginEnd="16dp"
  44.             android:layout_marginStart="16dp"
  45.             android:layout_marginTop="8dp"
  46.             app:layout_constraintBottom_toBottomOf="parent"
  47.             app:layout_constraintEnd_toEndOf="parent"
  48.             app:layout_constraintStart_toStartOf="parent"
  49.             app:layout_constraintTop_toBottomOf="@+id/repository_name"
  50.             app:layout_constraintVertical_bias="0.0"
  51.             tools:text="Mladen Rakonjac" />

  52.         <TextView
  53.             android:id="@+id/number_of_starts"
  54.             android:layout_width="wrap_content"
  55.             android:layout_height="wrap_content"
  56.             android:layout_marginBottom="8dp"
  57.             android:layout_marginEnd="16dp"
  58.             android:layout_marginStart="16dp"
  59.             android:layout_marginTop="8dp"
  60.             app:layout_constraintBottom_toBottomOf="parent"
  61.             app:layout_constraintEnd_toEndOf="parent"
  62.             app:layout_constraintHorizontal_bias="1"
  63.             app:layout_constraintStart_toStartOf="parent"
  64.             app:layout_constraintTop_toBottomOf="@+id/repository_owner"
  65.             app:layout_constraintVertical_bias="0.0"
  66.             tools:text="0 stars" />

  67.     </androidx.constraintlayout.widget.ConstraintLayout>

  68. </layout>
复制代码
注意,您必须将所有 xmlns 移动到 layout 标记。然后按下构建图标或使用快捷键 Cmd + F9。我们需要构建项目,以便数据绑定库能够生成类 ActivityMainBinding,我们将在 MainActivity 类中使用该类。

如果不执行项目的 Build,您将看不到 ActivityMainBinding 类,因为它是在编译时生成的。我们仍然没有完成绑定,我们只是说我们有一个非 null 变量,它是 ActivityMainBinding 类型。还有,你注意到我没有把?在 ActivityMainBinding 的末尾,我没有初始化它。这怎么可能?Lateinit 修饰符允许我们有非空变量等待初始化。与 ButterKnife 类似,绑定初始化应该在 onCreate 方法中完成,一旦布局就绪。此外,您不应该在 onCreate 方法中声明绑定,因为您可能会在 onCreate 方法范围之外使用它。我们的绑定不应该是 null,这就是为什么我们使用 lateinit。使用 lateinit 修饰符,我们不必每次访问绑定变量时都使用 null 检查。

让我们初始化绑定变量,你应该替换:
  1. //setContentView(R.layout.activity_main)
  2. var binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
复制代码
就是这样!您成功地绑定了您的视图。现在您可以访问它并应用一些更改。例如,让我们将存储库名称改为“ Modern Android Medium Article” :
  1. binding.repositoryName.text = "Modern Android Medium Article"
复制代码
正如您所看到的,我们可以通过绑定变量访问来自 activity _ main.xml 的所有视图(当然是具有 id 的视图)。这就是为什么数据绑定优于 ButterKnife 的原因。

Data binding library

Data binding library


也许你已经注意到我们没有像在java中一样使用setText ()方法。我想在这里停下来解释一下 getters 和 setters 在 Kotlin 与 Java 相比是如何工作的。

首先,您应该知道我们为什么使用 setters 和 getter。我们使用它来隐藏类的变量,只允许通过方法访问,这样我们就可以隐藏类的客户端的类的细节,并禁止同样的客户端直接更改我们的类。假设我们在 Java 中有 Square 类:

  1. public class Square {
  2.   private int a;
  3.   
  4.   Square(){
  5.     a = 1;
  6.   }

  7.   public void setA(int a){
  8.     this.a = Math.abs(a);
  9.   }
  10.   
  11.   public int getA(){
  12.     return this.a;
  13.   }
  14.   
  15. }
复制代码
原文实在是不知道怎么翻译,反正就是getter和setter这样写太麻烦了,我们都懂。
而使用Kotlin使我们的开发人员更加的容易,你可以这样写:
  1. var side: Int = square.a
复制代码
这样写的代码并不等于:
  1. int side = square.getA();
复制代码
因为 Kotlin 自动生成默认的 getter 和 setter。在 Kotlin,只有当你有特殊的 setter 或 getter 时,你才应该指定它。否则,Kotlin 会为你自动生成:
  1. var a = 1
  2.    set(value) { field = Math.abs(value) }
复制代码
现在这是什么? 为了说清楚,让我们看看这个代码:
  1. var a = 1
  2.    set(value) { a = Math.abs(value) }
复制代码
这意味着您在 set 方法中调用 set 方法,因为在 Kotlin 世界中不能直接访问该属性。这将产生无限递归。当您调用 a = 某个值时,它会自动调用 set method。我现在希望您清楚为什么必须使用 field 关键字以及 setter 和 getter 是如何工作的。
ps: 这一段不理解的话可以测试一下,不使用field的话会无限递归。

让我们回到我们的代码。我想向你们展示 Kotlin 语言的另一个重要特征:
  1. class MainActivity : AppCompatActivity() {

  2.     lateinit var binding: ActivityMainBinding

  3.     override fun onCreate(savedInstanceState: Bundle?) {
  4.         super.onCreate(savedInstanceState)

  5.         binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
  6.         binding.apply {
  7.             repositoryName.text = "Medium Android Repository Article"
  8.             repositoryOwner.text = "Mladen Rakonjac"
  9.             numberOfStarts.text = "1000 stars"

  10.         }
  11.     }
  12. }
复制代码
Apply 允许您对一个实例调用多个方法。
我们仍然没有完成数据绑定,还有更多的伟大的事情。让我们为 Repository 创建 UI 模型类(这是用于 Github Repository 的 UI Model 类,保存应该显示的数据,不要将其与 Repository 模式混淆)。要制作 Kotlin 类,你应该去 New-> Kotlin 文件/类:
  1. class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false)
复制代码
在 Kotlin中,主构造函数是类头的一部分。如果您不想提供第二个构造函数,那就这样吧!你的新建类的工作在这里就结束了。字段分配没有构造函数参数,也没有 getter 和 setter,全部都排成一排!

返回 MainActivity.kt 类,创建 Repository 类的实例:
  1. var repository = Repository("Medium Android Repository Article",
  2.         "Mladen Rakonjac", 1000, true)
复制代码


正如您所注意到的,对象构造没有new的关键字。

现在让我们转到 activity_main.xml 并添加 data tag:
  1. <data>
  2.       <variable
  3.         name="repository"
  4.         type="com.android.newkotlin.Repository"
  5.         />
  6. </data>
复制代码
我们可以访问布局中属于 Repository 类型的这个存储库变量。例如,我们可以使用 repository _ name id 在 TextView 中执行以下操作:
  1. android:text="@{repository.repositoryName}"
复制代码
Repository_name 文本视图将显示从 repository 变量的 repositoryName 属性获得的文本。剩下的唯一工作就是将库变量从 xml 绑定到 MainActivity.kt。按下 Build 来创建数据绑定库来生成所需的类,然后返回到 MainActivity 并添加以下两行:
  1. binding.repository = repository
  2. binding.executePendingBindings()
复制代码

如果你运行这个应用程序,你会看到文本视图将显示“ Medium Android Repository Article”

但是如果我们这样做:
  1. Handler().postDelayed({repository.repositoryName="New Name"}, 2000)
复制代码
2秒以后会显示新的文本吗?不,不会的。您必须再次设置存储库。类似这样的东西会起作用:
  1. Handler().postDelayed({repository.repositoryName="New Name"
  2.     binding.repository = repository
  3.     binding.executePendingBindings()}, 2000)
复制代码


但是,如果每次我们改变一些属性的时候都要这样做的话,那就太无聊了。有一个更好的解决方案叫做属性观察者。首先让我们描述一下什么是观察者模式/java,因为我们在 rxJava 部分也需要它: http://androidweekly.net/

也许,你已经听说了几个 http://androidweekly.net/了。这是一份关于 Android 开发的周刊。如果你想收到它,你必须订阅它给你的电子邮件地址。一段时间后,如果你决定了,你可以停止在他们的网站上的退订选项。

这是观察者/可观察模式的一个例子。在这种情况下,Android 周刊是可观察的,它每周发布新闻通讯。读者是观察者,因为他们订阅它,等待新的信息发出,一旦他们收到信息,他们就读它,如果他们中的一些人认为她不喜欢它,她/他就可以停止听它。

在我们的示例中,属性观察器是 xml 布局,它将侦听 Repository 实例中的更改。所以,仓库是可观察的。例如,一旦在 Repository 类的实例中更改了 Repository name 属性,就应该更新 xml 而不调用:
  1. binding.repository = repository
  2. binding.executePendingBindings()
复制代码


如何使用数据绑定库?数据绑定库为我们提供了库类应该实现的 BaseObservable 类:
  1. class Repository(repositoryName : String, var repositoryOwner: String?, var numberOfStars: Int?, var hasIssues: Boolean = false) : BaseObservable(){

  2.     @get:Bindable
  3.     var repositoryName : String = ""
  4.     set(value) {
  5.         field = value
  6.         notifyPropertyChanged(BR.repositoryName)
  7.     }

  8. }
复制代码

BR 是一个在使用可绑定注释时自动生成一次的类。正如您所看到的,在设置新值时,我们将通知它。现在您可以运行应用程序,您将看到存储库名称将在2秒钟后更改,而不再调用 executePendingBindings ()。
这部分就到此为止了。在下一部分,我将写 MVVM 模式和存储库模式,我将写 Android 包装器管理器。


分享到:
回复

使用道具 举报

回答|共 1 个

LazyGirl

发表于 2020-9-16 10:03:08 | 显示全部楼层

什么时候出下一部分啊?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则