Data Binding 數(shù)據(jù)綁定(一)


前幾天在忙一些其他的東西,DataBinding 這個(gè)系列的博客本應(yīng)該在五月月初就要寫的,結(jié)果一直拖到了現(xiàn)在,罪過罪過。
在學(xué)習(xí) DataBinding 的過程中,參考 Google 官方的 DataBinding 示例 Demo,自己寫了一個(gè) DataBindingPractice Demo,用于練手。整個(gè)工程采用 MVP 架構(gòu) + DataBinding,歡迎 star、fork 和溝通交流。
本文介紹了 Data Binding 的一些基本概念和基本用法,主要包括以下四部分內(nèi)容:

  1. Data Binding 的介紹
  2. Data Binding 中的布局文件
  3. Data Binding 中的事件處理
  4. Data Binding 中的布局詳情

Data Binding 的介紹

簡(jiǎn)介

在 Data Binding 庫之前,我們經(jīng)常會(huì)寫一些重復(fù)性很高而且毫無營養(yǎng)的代碼,比如:findViewById()、setText()、setOnClickListener() 等。使用 Data Binding 庫以后,可以使用聲明式布局文件來減少粘結(jié)業(yè)務(wù)邏輯和布局文件的膠水代碼。

  1. Data Binding 具有良好的靈活性和兼容性,它是一個(gè) support 庫,向后兼容至 Android 2.1(API Level 7+)。
  2. 若使用 Android Studio 開發(fā)環(huán)境開發(fā) Android 應(yīng)用程序,則必須滿足以下兩個(gè)條件才可以使用 Data Binding 庫:
* Gradle Plugin 版本必須是 1.5.0-alpha1 或以上的版本
* Android Studio 的版本必須是1.3或以上的版本。

Data Binding 環(huán)境構(gòu)建

在 Module 的 build.gradle 中添加如下代碼,這樣應(yīng)用就支持 Data Binding 庫了。

   android {
       ....
       dataBinding {
           enabled = true
       }
  }

注意:若 app Module 依賴了一個(gè)使用 Data Binding 的庫,則 app Module 的 build.gradle 也必須配置 Data Binding 庫。

Data Binding 中的布局文件

第一個(gè) data binding 表達(dá)式

與傳統(tǒng)的布局文件相比,data binding 布局文件與其只有輕微的不同,data binding 布局文件中的根元素是 <layout> 標(biāo)簽,其中包含一個(gè) <data> 標(biāo)簽和一個(gè) <view> 標(biāo)簽,這個(gè) <view> 標(biāo)簽的內(nèi)容與普通布局文件的內(nèi)容相同。如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>
  1. 在 `` 標(biāo)簽中的 user 變量是一個(gè)在這個(gè) data binding 布局文件中會(huì)用到的屬性。
  2. 在 data binding 布局文件中,data binding 表達(dá)式使用 @{} 語法。

數(shù)據(jù)對(duì)象(Data Object)

  1. 假設(shè)有一個(gè) User 類是 plain-old Java object (POJO) 類型的,如下所示:
public class User {
       public final String firstName;
       public final String lastName;
       public User(String firstName, String lastName) {
           this.firstName = firstName;
           this.lastName = lastName;
       }
 }
  1. 還有一個(gè) User 類,是 JavaBeans 類型的,如下所示:
public class User {
       private final String firstName;
       private final String lastName;
       public User(String firstName, String lastName) {
           this.firstName = firstName;
           this.lastName = lastName;
       }
       public String getFirstName() {
           return this.firstName;
       }
       public String getLastName() {
           return this.lastName;
       }
}
  1. 這兩個(gè) User 類對(duì)于 Data Binding 庫來說是等價(jià)的。在 TextView 中的 android:text 屬性 @{user.firstName} 會(huì)使用 POJO User類中的 firstName 字段,或者 JavaBeans User 類中的 getFirstName() 方法。

綁定對(duì)象(Binding Data)

  1. 默認(rèn)情況下,基于 data binding 布局文件會(huì)生成一個(gè) Binding 類,此 Binding 類是將布局文件的名稱轉(zhuǎn)換成帕斯卡命名,并在之后接上 Binding 命名的。比如,布局文件名稱是 activity_main.xml,則其對(duì)應(yīng)的 Binding 類是 ActivityMainBinding。這個(gè) Binding 類包含了布局文件中所有的布局屬性和布局視圖的綁定關(guān)系,并且知道如何向 data binding 表達(dá)式賦值。在 inflate 的時(shí)候,是創(chuàng)建 binding 關(guān)系最簡(jiǎn)單的時(shí)候,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
       // 下面這行生成 Binding 類的代碼和上面這行生成 Binding 類的代碼是等價(jià)的。
       // MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
       User user = new User("Test", "User");
       binding.setUser(user);
}
  1. 如果在 ListView 或者 RecyclerView 中的 Item 中使用 Data Binding,可以使用如下方式生成每個(gè) Item 對(duì)應(yīng)的 Binding 類,如下所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

Data Binding 中的事件處理

Data Binding 庫允許使用 data binding 表達(dá)式處理由 View 分發(fā)的事件。事件屬性的名字由 Listener 中的方法名稱決定。例如,在 View.onLongClickListener() 中有一個(gè) onLongClick() 方法,則這個(gè)事件對(duì)應(yīng)的屬性名稱是 android:onLongClick。有兩種方法處理一個(gè)事件:

  • 方法引用:在表達(dá)式中,可以引用符合監(jiān)聽器方法簽名的處理方法。
  • 監(jiān)聽綁定:在表達(dá)式中,是使用一個(gè) Lambda 表達(dá)式處理事件的。

兩者的區(qū)別,官方說法:方法引用和監(jiān)聽綁定的主要區(qū)別是,方法引用中監(jiān)聽器的實(shí)現(xiàn)是在數(shù)據(jù)綁定期間完成的,而不是在觸發(fā)事件時(shí)創(chuàng)建的。如果更偏向于在事件發(fā)生時(shí)再計(jì)算表達(dá)式的值,則應(yīng)該使用監(jiān)聽綁定??梢岳斫鉃椋悍椒ㄒ檬窃诰幾g期處理,而監(jiān)聽綁定是在事件分發(fā)時(shí)處理。

方法引用(Method References)

  1. 在方法引用中,可以直接將事件綁定到一個(gè)處理類的方法上去,類似于 android:onClick 可以指定到一個(gè) Activity 中的方法。和 View.onClick 相比,方法引用表達(dá)式的一個(gè)主要優(yōu)點(diǎn)是:方法引用是在編譯期處理的,所以如果引用的方法不存在或者方法的簽名不匹配的話,在編譯期就會(huì)報(bào)錯(cuò)。
  2. 若要將一個(gè)事件指派給一個(gè)處理類,則需要使用一個(gè)正常的 data binding 表達(dá)式,這個(gè) data binding 表達(dá)式的值是將要調(diào)用的方法的名稱。例如,有一個(gè)類如下所示:
public class MyHandlers {
        public void onClickFriend(View view) { ... }
}

data binding 表達(dá)式可以為 View 指定點(diǎn)擊監(jiān)聽器,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
      <variable name="handlers" type="com.example.MyHandlers"/>
      <variable name="user" type="com.example.User"/>
  </data>
  <LinearLayout
      android:orientation="vertical"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"
          android:onClick="@{handlers::onClickFriend}"/>
  </LinearLayout>
</layout>

注意:在 data binding 表達(dá)式中的方法簽名必須和監(jiān)聽器對(duì)象中的方法簽名匹配,引用的方法的參數(shù)必須事件監(jiān)聽器的方法的參數(shù)匹配。

監(jiān)聽綁定(Listener Bindings)

監(jiān)聽綁定是在事件發(fā)生時(shí)才會(huì)運(yùn)行的 data binding 表達(dá)式。

  • 監(jiān)聽綁定在 Gradle Plugin 2.0及更新的版本上才可以使用
  • 和方法引用類似,不過它允許你運(yùn)行任意數(shù)據(jù)綁定表達(dá)式(不限制處理方法的參數(shù))
  • 在監(jiān)聽綁定中,引用的方法的返回值和事件監(jiān)聽器期望的返回值匹配即可(除非它期望是void的)
  1. 例如,有一個(gè) Presenter 類如下所示:
public class Presenter {
        public void onSaveClick(Task task){}
}

可以將點(diǎn)擊事件綁定到這個(gè) presenter 類上,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="task" type="com.android.example.Task" />
        <variable name="presenter" type="com.android.example.Presenter" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"  
        android:layout_height="match_parent">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onSaveClick(task)}" />
    </LinearLayout>
</layout>
  • 監(jiān)聽器由 Lambda 語句表達(dá),并且只允許作為表達(dá)式的根元素使用。
  • 如果在表達(dá)式中會(huì)使用一個(gè)回調(diào),Data Binding會(huì)自動(dòng)的創(chuàng)建必要的監(jiān)聽器并將其注冊(cè)到對(duì)應(yīng)的事件。當(dāng)該控件的事件發(fā)生時(shí),Data Binding會(huì)計(jì)算表達(dá)式的值。
  • 在常規(guī)的綁定表達(dá)式中,當(dāng)監(jiān)聽器的表達(dá)式計(jì)算式,Data Binding會(huì)保證綁定表達(dá)式中引用變量的空值安全性和線程安全性。
  • 請(qǐng)注意,在上面的例子中,沒有定義傳遞進(jìn) onClick() 中的 View 參數(shù)。監(jiān)聽綁定為監(jiān)聽器的參數(shù)提供了兩種選擇:要么把參數(shù)全部寫上,要么把參數(shù)全部忽略不寫。如果傾向于寫出全部的參數(shù),則上面的例子該像下面這樣寫:
  android:onClick="@{(view) -> presenter.onSaveClick(task)}" 

如果想在表達(dá)式中使用參數(shù),則可以像下面代碼一樣使用:

  public class Presenter {
          public void onSaveClick(View view, Task task){}
  }
android:onClick="@{(view) -> presenter.onSaveClick(view, task)}"
  1. 還可以使用具有多個(gè)參數(shù)的 Lambda 表達(dá)式:
public class Presenter {
        public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox
      android:layout_width="wrap_content"   
      android:layout_height="wrap_content"
      android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
  1. 如果正在監(jiān)聽的事件函數(shù)的返回值是非空的,則綁定表達(dá)式的值也必須返回相同類型的值。例如,正在監(jiān)聽 onLongClick() 事件函數(shù),則綁定表達(dá)式需要返回 boolean 型的。如下所示:
public class Presenter {
        public boolean onLongClick(View view, Task task){}
}
 android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
  • 如果由于空對(duì)象而無法計(jì)算綁定表達(dá)式的值,則 Data Binding 返回 Java 中默認(rèn)的值,例如:引用型對(duì)象則返回 null, int 型則返回0,Boolean 型則返回 false 等等。
  • 如果需要使用帶斷言(例如三元表達(dá)式)的表達(dá)式,則可以使用void作為空操作符號(hào)。例如:
  android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"    

避免復(fù)雜的監(jiān)聽

  1. 監(jiān)聽器表達(dá)式是非常強(qiáng)大的,會(huì)讓你的代碼非常容易閱讀。
  2. 另一方面,如果監(jiān)聽器中包含復(fù)雜的表達(dá)式則會(huì)讓布局文件難以閱讀和維護(hù),所以布局文件中的表達(dá)式應(yīng)盡可能簡(jiǎn)單,表達(dá)式只是調(diào)用回調(diào)方法,而具體的業(yè)務(wù)邏輯應(yīng)該寫在回調(diào)方法中。
  3. 有個(gè)別點(diǎn)擊事件的監(jiān)聽器回調(diào)函數(shù)的方法名稱和 View 的 android:onClick 相同,下面有一些新的屬性名稱,用于避免沖突:
Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

Data Binding 中的布局詳情

導(dǎo)入(Imports)

  1. 在布局文件中的 data 標(biāo)簽中可以使用 import 標(biāo)簽導(dǎo)入類,就像在 java 文件中導(dǎo)入類一樣,如下所示:
<data>
        <import type="android.view.View"/>
</data>
<TextView
        android:text="@{user.lastName}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
  1. 當(dāng)導(dǎo)入的類名沖突時(shí),可以使用 alias 屬性為類起個(gè)別名,如下所示:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>
  • 別名只在此布局文件內(nèi)有效。導(dǎo)入的類,可以在 data binding 表達(dá)式中使用,也可以在申明變量時(shí)使用。
  • 目前,在 Android Studio 中,并沒有提供 Data Binding 在布局文件中導(dǎo)入類自動(dòng)補(bǔ)全的功能。如果在布局文件中使用的類,沒有被導(dǎo)入,編譯可以正常通過,但是運(yùn)行時(shí)會(huì)出現(xiàn)問題??梢酝ㄟ^在申明變量時(shí),使用完全限定名類避免這個(gè)問題隱患。
  1. 在 data binding 表達(dá)式中可以使用導(dǎo)入類的靜態(tài)方法和靜態(tài)字段,如下所示:
<data>
        <import type="com.example.MyStringUtils"/>
        <variable name="user" type="com.example.User"/>
</data>
    …
    <TextView
        android:text="@{MyStringUtils.capitalize(user.lastName)}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  1. 和在 Java 中一樣,java.lang.* 包下的類會(huì)被自動(dòng)導(dǎo)入。

變量(Variables)

  1. data 標(biāo)簽中,可以定義任意數(shù)量的變量,每個(gè)變量都可以被該布局文件中的任意一個(gè) data binding 表達(dá)式使用。如下所示:
  <data>
        <import type="android.graphics.drawable.Drawable"/>
        <variable name="user"  type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note"  type="String"/>
</data>
  1. Data Binding 在編譯期內(nèi)會(huì)對(duì)申明的變量檢查類型,如果該變量實(shí)現(xiàn)了 Observable 接口,或者是一個(gè) Observable 集合類型的,那它應(yīng)該在類型上反映出來。若是一個(gè)沒有實(shí)現(xiàn) Observable 的基礎(chǔ)類或者基礎(chǔ)接口,則該類不會(huì)被觀察。
  2. 自動(dòng)生成的 binding 類會(huì)為每個(gè)變量生成對(duì)應(yīng)的 setter 和 getter 方法,在每個(gè)變量的 setter 方法被調(diào)用之前,該變量將會(huì)采用默認(rèn)值,即:引用型變量默認(rèn)值是null, int 型變量默認(rèn)值是0,boolean 型變量的默認(rèn)值是 false。
  3. Data Binding 會(huì)生成一個(gè)特殊的名為 context 的變量,以便 data binding 表達(dá)式在需要的時(shí)候使用,此 context 變量的值其實(shí)就是該布局文件中 rootViewgetContext() 的返回值。
  4. 如果在該布局文件中有一個(gè)名為 context 的變量,則 Data Binding 生成的 context 將會(huì)被覆蓋。

自定義 Binding 類的名稱(Custom Binding Class Names)

  1. Data Binding 會(huì)為每一個(gè)使用了 Data Binding 的布局文件生成一個(gè)對(duì)應(yīng)的 Binding 類,該類的名稱是基于布局文件的名稱的,采用大駝峰命名規(guī)則,移除下劃線_,并在最后追加 Binding,這個(gè)類會(huì)被放在該 Module 包的 databinding 包中。例如:如果一個(gè)名為 activity_main.xml 的布局文件使用了 Data Binding 庫,則 Data Binding 庫會(huì)自動(dòng)生成一個(gè)名為ActivityMainBinding 的 Binding 類,如果該 Module 的包名為 com.lijiankun24.databindingpractice,則 ActivityMainBinding 類在 com.lijiankun24.databindingpractice.databinding 包下。
  2. 通過修改 data 標(biāo)簽的 class 屬性,就可以修改 binding 類的名稱和位置。
  • 若像下面這樣:
<data class="ActivityCustomBinding">
  ...
</data>

則該布局文件對(duì)應(yīng)的 Binding 類的名稱是 ActivityCustomBinding,而與該布局文件的名稱無關(guān)。

  • 若像下面這樣:
 <data class=".ActivityCustomBinding">
   ...
 </data>

則該布局文件對(duì)應(yīng)的 Binding 類會(huì)被放在該 Module 包下,而不是該 Module 的 databinding 包下。

  • 若像下面這樣:
  <data class="com.example.ActivityCustomBinding">
    ...
  </data>

則可以任意地指定該布局文件對(duì)應(yīng)的 Binding 類所在的位置。

Includes 標(biāo)簽

  1. data 標(biāo)簽中聲明的變量,可以通過應(yīng)用命名空間將該變量傳遞到被 include 的布局中。如:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>

如上面代碼所示,可以將在 data 標(biāo)簽中聲明的 user 變量傳遞到name.xmlcontact.xml 布局文件中,前提是在這兩個(gè)布局文件中必須也聲明了 user 變量。

  1. Data Binding 庫并不支持 merge 標(biāo)簽直接做為其子元素,如下所示的代碼是不允許的。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <merge>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

表達(dá)式語法(Expression Language)

通用特性

表達(dá)式語言和 Java 表達(dá)式有很多相似之處,如下所示:

  • 數(shù)學(xué)運(yùn)算符:+ - * / %
  • 字符串連接符:+
  • 邏輯運(yùn)算符:&& ||
  • 位運(yùn)算符:& | ^
  • 一元操作符:+ - ! ~
  • 移位運(yùn)算: >> >>> <<
  • 比較運(yùn)算符:== > < >= <=
  • 實(shí)例判斷:instanceof
  • 組:()
  • Literals - character, String, numeric, null
  • 類型轉(zhuǎn)換 Cast
  • 方法調(diào)用 Method calls
  • 字段存取 Field access
  • 數(shù)組存取 Array access: []
  • 三目運(yùn)算符:?:
    如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

不支持的操作符

一些在 Java 中的操作符,在 Binding 表達(dá)式中不支持,如下:

  • this
  • super
  • new
  • 顯式泛型調(diào)用

空合并運(yùn)算符(Null Coalescing Operator)

空合并運(yùn)算符(??): 如果左操作數(shù)不為空,則選擇左操作數(shù)否則選擇右操作數(shù)。

android:text="@{user.displayName ?? user.lastName}"

上面代碼等價(jià)于:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

空指針異常處理(Avoiding NullPointerException)

Data Binding 生成的代碼中會(huì)自動(dòng)檢查 null 并避免空指針異常。例如,在 data binding 表達(dá)式 @{user.name} 中,如果 user 變量是 null 的,若 name 是 String 類型的,則將為 user.name 分配其默認(rèn)值 null;若引用了 user.age,其中 age 是 int 型的,那么它的默認(rèn)值是0。

集合(Collections)

可以使用 [] 操作符來操作通用的容器類,比如:arrays, lists, sparse lists 和 maps,如:

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List<String>"/>
    <variable name="sparse" type="SparseArray<String>"/>
    <variable name="map" type="Map<String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

字符串語法(String Literals)

  1. 當(dāng)屬性值使用單引號(hào)括起來時(shí),在表達(dá)式中需要使用雙引號(hào)。
android:text='@{map["firstName"]}'
  1. 屬性值也可以使用雙引號(hào)括起來,則表達(dá)式中的字符串應(yīng)該使用 ' 或者后引號(hào) ` ,如:
android:text="@{map[`firstName`}"android:text="@{map['firstName']}"

資源(Resources)

  1. 在表達(dá)式中可以使用正常的語法引用資源。
android:padding="@{large ? @dimen/largePadding : @dimen/smallPadding}"
  1. 在字符串格式化和復(fù)數(shù)形式中可以使用參數(shù),如:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
  1. 當(dāng)復(fù)數(shù)形式中有多個(gè)參數(shù)是,多個(gè)參數(shù)必須同時(shí)傳遞進(jìn)去,如:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
  1. 一些資源需要在表達(dá)式中使用特定引用類型,如:
Type Normal Reference Expression Reference
String[] @array @stringArrayint[]
array @intArrayTypedArray @array
typedArrayAnimator @animator @animatorStateListAnimator
animator @stateListAnimatorcolor int @color
colorColorStateList @color @colorStateList

DataBinding 第一篇文章先介紹這些,如果有什么問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com


參考資料:

DataBInding 官方文檔

深入Android Data Binding(一):使用詳解 -- YamLee

Android Data Binding 系列(一) -- 詳細(xì)介紹與使用 -- ConnorLin

DataBinding(一)-初識(shí) -- sakasa(譯)Data Binding 指南 -- 楊輝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容