Android快速轉戰Kotlin教程

前言

kotlin是啥?這裡就不用多說了,想必看這篇文章的童鞋肯定是有所瞭解的。

那麼這篇文章你可以收穫什麼?

答:本文主要通過本人如何從java轉戰到kotlin並應用在實際專案中的個人經歷,給大家提供一些學習思路、學習方法以及一些學習資料和個人總結。

前提:你的專案(包含個人專案)即將開始用到kotlin(沒有專案作為依託你會缺少十足的動力,而且缺少應用場景乘熱打鐵那也是白學)
建議:建議沒有切換kotlin的小夥伴快來轉戰kotlin吧!最近一段時間搞了kotlin之後發現寫起來確實比java爽多了,語法非常精簡,而且據統計現已有30%安卓專案使用了kotlin,所以小夥伴們行動起來吧,這必定是大勢所趨,可千萬別被淘汰了啊

入門

俗話說萬事開頭難,不過我們先把Kotlin語法學習一遍,你就會發現so easy,而且語言思想都是相通的

第一步:學習語法

當然是去官網學習嘍:http://kotlinlang.org/docs/reference/

如下圖:

不過英文吃力的小夥伴可以去菜鳥教程網站學習

地址:http://www.runoob.com/kotlin/kotlin-tutorial.html

如下圖:

內容與官網一致。

不過不能光看,一定要寫,就算照著抄也要多寫,儘量在學習時候多和java語法做對比,會印象深刻。
如下圖,本人的練習程式碼:

第二步:對比學習

大家可以參考下面的連結進行學習:

from-java-to-kotlin : https://github.com/MindorksOpenSource/from-java-to-kotlin

from-java-to-kotlin中給出了我們常用的語法對比

如圖:

第三步:Demo練習

通過上面的學習我們此刻已經熟悉了kotlin的基本語法,可以來嘗試寫一個萬年曆的Demo。

1、新建工程

我們新建一個工程,點選Include kotlin support
如圖:

我們看一下Include kotlin support都幫我們做了什麼事情

首先module中gradle檔案

如圖:

比我們之前的工程多了下面兩個引用和一個依賴:

// 使用Kotlin外掛
apply plugin: 'kotlin-android'
// 使用Kotlin Android擴充套件外掛
apply plugin: 'kotlin-android-extensions'
dependencies {
//...
//新增Kotlin 標準庫
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
//...
}

知識點: kotlin-android-extensions相當於DataBinding,同樣的目的為了偷懶不用寫findViewByIdAndroid 開發必備。

我們再看一下project中的gradle檔案
如圖:

比我們之前的工程多了Kotlin編譯外掛:

// 新增了Kotlin編譯外掛
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

2、Demo說明

該專案使用MVP模式,裡面用到了Retrofit2 RxJava2,然後使用了聚合的萬年曆介面,Demo非常簡單便於初學者快速掌握。

Demo使用展示:

工程目錄結構如圖:

3、Activity

看下佈局檔案非常簡單,我們可以在activity裡面直接將控制元件的id當成變數來使用

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
....">
<DatePicker
android:id="@ id/dataPicker"
.... />
<Button
android:id="@ id/selectButton"
.... />
<TextView
android:id="@ id/titleTextView"
.... />
<TextView
android:id="@ id/contentTextView"
....
/>
</android.support.constraint.ConstraintLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
selectButton.setOnClickListener {
titleTextView.visibility = View.GONE
selectButton.visibility = View.GONE
contentTextView.visibility = View.GONE
dataPicker.visibility = View.VISIBLE
}
....
....
}

注意:直接使用id作為變數的時候,要在Module的gradle裡面加入擴充套件,才能使用,不然會報錯

apply plugin: 'kotlin-android-extensions'

這個上面已經說過,我們建立工程的時候如果選中Include kotlin support怎會自動在gradle中生成。

4、Retrofit RxJava

Retrofit結合RxJava能快捷的使用網路請求。

建立Service介面,Kotlin的型別是寫在後面

interface RetrofitService {
/**
*  獲取當天詳細資訊
*  @param date 日期
*/
@GET("calendar/day")
fun calenderDay(
@Query("date") date: String,
@Query("key") key: String
): Observable<CalentarDayBean>
/**
*  獲取近期假期
*  @param date 日期
*/
@GET("calendar/month")
fun calenderMonth(
@Query("date") date: String
): Observable<CalentarMonthBean>
/**
*  獲取當年假期列表
*  @param date 日期
*/
@GET("calendar/year")
fun calenderYear(
@Query("date") date: String
): Observable<CalentarYearBean>
}

建立Retrofit,Kotlin的class並不支援static變數,所以需要使用companion object來宣告static變數,其實這個變數也不是真正的static變數,而是一個伴生物件

伴生物件可以實現靜態呼叫,通過類名.屬性名或者類名.方法名進行呼叫

class RetrofitUtil {
companion object {
/**
* 建立Retrofit
*/
fun create(url: String): Retrofit {
//日誌顯示級別
val level: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY
//新建log攔截器
val loggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
message -> Logger.e("OkHttp: "   message)
})
loggingInterceptor.level = level
// okHttpClientBuilder
val okHttpClientBuilder = OkHttpClient().newBuilder()
okHttpClientBuilder.connectTimeout(60, TimeUnit.SECONDS)
okHttpClientBuilder.readTimeout(10, TimeUnit.SECONDS)
//OkHttp進行新增攔截器loggingInterceptor
//okHttpClientBuilder.addInterceptor(loggingInterceptor)
return Retrofit.Builder()
.baseUrl(url)
.client(okHttpClientBuilder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
val retrofitService: RetrofitService = RetrofitUtil.getService(Constants.REQUEST_BASE_URL, RetrofitService::class.java)
/**
* 獲取ServiceApi
*/
fun <T> getService(url: String, service: Class<T>): T {
return create(url).create(service)
}
}
}

通過伴生物件,結合Retrofit結合RxJava 我們直接就可以呼叫介面了

RetrofitUtil
.retrofitService
.calenderDay(date,"933dc930886c8c0717607f9f8bae0b48")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
view?.showDayCalentarData(result)
Logger.e(result.toString())
}, { error ->
view?.showError(error.message.toString())
Logger.e(error.message.toString())
})

5、使用物件宣告

在寫專案的時候,一般會將常量統一寫到一個類裡面,然後設定靜態變數,由於在Kotlin中不存在靜態變數,所有就有物件宣告的存在,物件宣告比較常用的地方就是在這裡,物件宣告用Objcet關鍵字表示。

object Constants {
val REQUEST_BASE_URL = "http://v.juhe.cn/"
val KEY = "1be865c0e67e3"
}

使用的時候直接類名加.加變數名,如Constants.REQUEST_BASE_URL

6、使用資料類

Kotlin有專門的資料類,就是用data修飾的類
首先我們先看一下json資料:

{
"reason":"Success",
"result":{
"data":{
"date":"2018-4-4",
"weekday":"星期三",
"animalsYear":"狗",
"suit":"訂盟.納采.冠笄.拆卸.修造.動土.安床.入殮.除服.成服.移柩.安葬.破土.啟攢.造倉.",
"avoid":"作灶.開光.嫁娶.開市.入宅.",
"year-month":"2018-4",
"lunar":"二月十九",
"lunarYear":"戊戌年"
}
},
"error_code":0
}

再來看一下我的資料類:

data class CalentarDayBean(
val reason: String,
val result: CalentarDayResult,
val error_code: Int
)
data class CalentarDayResult(
val data: CalentarDayData
)
data class CalentarDayData(
val date: String,
val weekday: String,
val animalsYear: String,
val suit: String,
val avoid: String,
val yearMonth: String,
val holiday: String,
val lunar: String,
val lunarYear: String,
val desc: String
)

就是如此方便

7、MVP

kotlin的MVP和java原理一模一樣我先定義了IBaseModelIBaseView

IBaseModel

interface IBaseModel<T> {
fun onDestroy()
fun attachView(view: T)
}

IBaseView

interface IBaseView {
fun showLoading()
fun hideLoading()
fun showMessage(message: String)
fun killMyself()
}

然後完成ICalentarContract,這個類似合同類的介面把P和V的所有方法全部寫在一起,看起來程式碼格外清楚

interface ICalentarContract {
/**
* 對於經常使用的關於UI的方法可以定義到IBaseView中,如顯示隱藏進度條,和顯示文字訊息
*/
interface View : IBaseView {
fun showDayCalentarData(calentarDayBean: CalentarDayBean)
fun showError(errorMsg: String)
}
/**
* Model層定義介面,外部只需關心Model返回的資料,無需關心內部細節,如是否使用快取
*/
interface Model : IBaseModel<ICalentarContract.View> {
fun getDayCalentarData(date: String)
}
}

然後activity去實現ICalentarContract.View,presenter去實現ICalentarContract.Model

class CalentarDatePresenter : ICalentarContract.Model {
....
}
class MainActivity : AppCompatActivity(), ICalentarContract.View {
...
}

so easy~~~ 到這裡我們的Demo就完成了,可以盡情玩樂。

專案地址:待上傳。。。。。。。。。。。。。

好了,到這裡我們基本掌握了Kotlin在安卓中的應用,那麼接下來就需要去學習一下kotlin設計模式以及一些進階知識~

進階

一、Kotlin設計模式

本文只列出幾個常用的設計模式

1、觀察者模式( observer pattern )

Example

interface TextChangedListener {
fun onTextChanged(newText: String)
}
class PrintingTextChangedListener : TextChangedListener {
override fun onTextChanged(newText: String) = println("Text is changed to: $newText")
}
class TextView {
var listener: TextChangedListener? = null
var text: String by Delegates.observable("") { prop, old, new ->
listener?.onTextChanged(new)
}
}

Usage

val textView = TextView()
textView.listener = PrintingTextChangedListener()
textView.text = "Lorem ipsum"
textView.text = "dolor sit amet"

Output

Text is changed to: Lorem ipsum
Text is changed to: dolor sit amet

2、策略模式( strategy pattern )

Example

class Printer(val stringFormatterStrategy: (String) -> String) {
fun printString(string: String) = println(stringFormatterStrategy.invoke(string))
}
val lowerCaseFormatter: (String) -> String = { it.toLowerCase() }
val upperCaseFormatter = { it: String -> it.toUpperCase() }

Usage

val lowerCasePrinter = Printer(lowerCaseFormatter)
lowerCasePrinter.printString("LOREM ipsum DOLOR sit amet")
val upperCasePrinter = Printer(upperCaseFormatter)
upperCasePrinter.printString("LOREM ipsum DOLOR sit amet")
val prefixPrinter = Printer({ "Prefix: "   it })
prefixPrinter.printString("LOREM ipsum DOLOR sit amet")

Output

lorem ipsum dolor sit amet
LOREM IPSUM DOLOR SIT AMET
Prefix: LOREM ipsum DOLOR sit amet

3、單例模式(singleton pattern)

Example

class Singletone private constructor() {
init {
println("Initializing with object: $this")
}
companion object {
val getInstance =SingletonHolder.holder
}
private object SingletonHolder {
val holder = Singletone()
}
fun print() = println("Printing with object: $this")
}

Usage

Singletone.getInstance.print()
Singletone.getInstance.print()

Output

Initializing with object: [email protected]
Printing with object: [email protected]
Printing with object: [email protected]

4、工廠模式(Factory Method)

Example

interface Currency {
val code: String
}
class Euro(override val code: String = "EUR") : Currency
class UnitedStatesDollar(override val code: String = "USD") : Currency
enum class Country {
UnitedStates, Spain, UK, Greece
}
class CurrencyFactory {
fun currencyForCountry(country: Country): Currency? {
when (country) {
Country.Spain, Country.Greece -> return Euro()
Country.UnitedStates          -> return UnitedStatesDollar()
else                          -> return null
}
}
}

Usage

val noCurrencyCode = "No Currency Code Available"
val greeceCode = CurrencyFactory().currencyForCountry(Country.Greece)?.code() ?: noCurrencyCode
println("Greece currency: $greeceCode")
val usCode = CurrencyFactory().currencyForCountry(Country.UnitedStates)?.code() ?: noCurrencyCode
println("US currency: $usCode")
val ukCode = CurrencyFactory().currencyForCountry(Country.UK)?.code() ?: noCurrencyCode
println("UK currency: $ukCode")

Output

Greece currency: EUR
US currency: USD
UK currency: No Currency Code Available

5、代理模式(Protection Proxy)

Example

interface File {
fun read(name: String)
}
class NormalFile : File {
override fun read(name: String) = println("Reading file: $name")
}
//Proxy:
class SecuredFile : File {
val normalFile = NormalFile()
var password: String = ""
override fun read(name: String) {
if (password == "secret") {
println("Password is correct: $password")
normalFile.read(name)
} else {
println("Incorrect password. Access denied!")
}
}
}

Usage

val securedFile = SecuredFile()
securedFile.read("readme.md")
securedFile.password = "secret"
securedFile.read("readme.md")

Output

Incorrect password. Access denied!
Password is correct: secret
Reading file: readme.md

6、建造者模式(builder pattern)

Example

// Let's assume that Dialog class is provided by external library.
// We have only access to Dialog public interface which cannot be changed.
class Dialog() {
fun showTitle() = println("showing title")
fun setTitle(text: String) = println("setting title text $text")
fun setTitleColor(color: String) = println("setting title color $color")
fun showMessage() = println("showing message")
fun setMessage(text: String) = println("setting message $text")
fun setMessageColor(color: String) = println("setting message color $color")
fun showImage(bitmapBytes: ByteArray) = println("showing image with size ${bitmapBytes.size}")
fun show() = println("showing dialog $this")
}
//Builder:
class DialogBuilder() {
constructor(init: DialogBuilder.() -> Unit) : this() {
init()
}
private var titleHolder: TextView? = null
private var messageHolder: TextView? = null
private var imageHolder: File? = null
fun title(init: TextView.() -> Unit) {
titleHolder = TextView().apply { init() }
}
fun message(init: TextView.() -> Unit) {
messageHolder = TextView().apply { init() }
}
fun image(init: () -> File) {
imageHolder = init()
}
fun build(): Dialog {
val dialog = Dialog()
titleHolder?.apply {
dialog.setTitle(text)
dialog.setTitleColor(color)
dialog.showTitle()
}
messageHolder?.apply {
dialog.setMessage(text)
dialog.setMessageColor(color)
dialog.showMessage()
}
imageHolder?.apply {
dialog.showImage(readBytes())
}
return dialog
}
class TextView {
var text: String = ""
var color: String = "#00000"
}
}

Usage

//Function that creates dialog builder and builds Dialog
fun dialog(init: DialogBuilder.() -> Unit): Dialog {
return DialogBuilder(init).build()
}
val dialog: Dialog = dialog {
title {
text = "Dialog Title"
}
message {
text = "Dialog Message"
color = "#333333"
}
image {
File.createTempFile("image", "jpg")
}
}
dialog.show()

Output

setting title text Dialog Title
setting title color #00000
showing title
setting message Dialog Message
setting message color #333333
showing message
showing image with size 0
showing dialog [email protected]

2、相關書籍

個人認為還是需要找一本書籍好好地閱讀一遍,一下提供了相關書籍可以選擇適合自己的。

NO.1

《Kotlin for Android Developers》

Kotlin是編寫Android應用程式的新官方語言,多虧了這本書,你很快就能寫出程式碼。直奔主題,實用和完整的例子,它將在開發Android應用程式的同時展示你的語言。學習Kotlin並開始使用這個強大而現代的語言再次享受Android開發。

NO.2

《Kotlin開發快速入門與實戰》

學習本書之前不需要具備任何的計算機專業背景,任何有志於APP開發的讀者都能利用本書從頭學起。

資深軟體開發工程師根據Kotlin最新版本撰寫,系統講解Kotlin開發技巧和專案實戰。全書共分為7章,內容層次清晰,難度循序漸進。希望通過閱讀本書,能夠讓你成為一個全棧工程師。

NO.3

《瘋狂Kotlin講義》

本書尤其適合從Java轉Kotlin的讀者,對於沒有Java功底的讀者,可忽略“對比”部分,直接學習本書也可掌握Kotlin程式設計。

本書對Kotlin的解讀十分系統、全面,超過Kotlin官方文件本身覆蓋的內容。本書很多地方都會結合Java位元組碼進行深入解讀,比如對Kotlin擴充套件的解讀,對Kotlin主、次構造器的解讀,這種解讀目的不止於教會讀者簡單地掌握Kotlin的用法,而是力求讓讀者深入理解Kotlin,且更好地理解Java。

NO.4

《Kotlin實戰》

本書主要面向有一定Java 經驗的開發者。

本書將從語言的基本特性開始,逐漸覆蓋其更多的高階特性,尤其注重講解如何將 Koltin 整合到已有 Java 工程實踐及其背後的原理。本書分為兩個部分。第一部分講解如何開始使用 Kotlin 現有的庫和API,包括基本語法、擴充套件函式和擴充套件屬性、資料類和伴生物件、lambda 表示式,以及資料型別系統(著重講解了可空性和集合的概念)。第二部分教你如何使用 Kotlin 構建自己的 API,以及一些深層次特性——約定和委託屬性、高階函式、泛型、註解和反射,以及領域特定語言的構建。

本書適合廣大移動開發者及入門學習者,尤其是緊跟主流趨勢的前沿探索者。

NO.5

《揭祕Kotlin程式設計原理》

本書深入介紹Kotlin物件導向設計的語法特性及其背後的實現方式。

在本書中,讀者不僅能清晰地瞭解Kotlin的語法、高階特性,還能真正地掌握Kotlin背後的實現機制和設計哲學,形成對Kotlin語言既直觀、又深刻的認識——在此基礎上,讀者能準確、快速地上手實踐,大大提升自己的移動開發能力。

Kotlin的這些特性和實現機制,可以幫助開發者掃清開發道路上的一些障礙,讓開發變得更加簡單!本書是一本值得擁有,能切實幫助讀者加薪提職的好書!

專案

學習一門語言最快的方式就是看其如何在實際專案中運用,有了上面的基礎和進階,下面我們看一些開源專案:

1.Kotlin-for-Android-Developers(★1676)

介紹:這個專案其實是Kotlin-for-Android-Developers這本書的配套程式碼,如果你是kotlin的初學者,那麼這絕對是你學習kotlin的不二之選。專案通過一個天氣的例子很好的展示了kotlin帶來的強大功能,比如網路資料的請求,資料的快取設計,資料庫的操作,各種擴充套件函式的妙用等等。

地址:https://github.com/antoniolg/Kotlin-for-Android-Developers

2.Bandhook-Kotlin (★1494)

介紹:Kotlin版本的音樂播放器,資料來源於LastFm。

地址:https://github.com/antoniolg/Bandhook-Kotlin

3.GankClient-Kotlin (★1216)

介紹:gank.io kotlin實現的乾貨集中營Android客戶端,風格採用了Material Design。

地址:https://github.com/githubwing/GankClient-Kotlin

4.PoiShuhui-Kotlin(★897)

介紹:一個用Kotlin寫的簡單漫畫APP。

地址:https://github.com/wuapnjie/PoiShuhui-Kotlin

5.Eyepetizer-in-Kotlin(★1167)

介紹:Kotlin版本的Eyepetizer客戶端

地址:https://github.com/LRH1993/Eyepetizer-in-Kotlin

6.Tucao(★792)

介紹:Kotlin版本的吐槽客戶端

地址:https://github.com/blackbbc/Tucao

資源

一、重要資源

Kotlin 官網

https://kotlinlang.org/docs/reference/

Kotlin 官方網站是學習 Kotlin 好去處。在參考部分,你可以找到該語言的所有概念和功能的深入解析文件。在教程部分有關於設定工作環境並使用編譯器的實用分步指南。

這裡還有個 Kotlin 編譯器,是一個瀏覽器 APP,你可以在上面嘗試使用這門語言。它能載入許多示例,包括 Koans 課程 — 這是目前熟悉 Kotlin 語法的最好方式。

Kotlin 官博

https://blog.jetbrains.com/kotlin/

Kotlin 的官方部落格由 JetBrains 的一位作者負責。你可以在這裡找到所有與 Kotlin 相關的新聞、更新、教程、使用技巧等的內容。

在 Android 上開始使用 Kotlin

https://developer.android.com/kotlin/get-started.html

一篇很牛叉的文章,向我們展示瞭如何使用 Kotlin 編寫和執行 Android 應用程式的測試

從 Java 到 Kotlin

https://github.com/MindorksOpenSource/from-java-to-kotlin

實用的快速提醒列表工具包含了一些簡短的程式碼塊,藉由這個來幫助你快速找到通用 Java 操作符、功能以及宣告的 Kotlin 替代方案。

Kotlin 教學外掛

https://blog.jetbrains.com/kotlin/2016/03/kotlin-educational-plugin/

用於 IntelliJ IDEa 的外掛,可讓你在本地離線環境下使用 Koans 課程。

Kotlin on GitHub

https://github.com/jetbrains/kotlin

Kotlin 於 2012 年開源,你可以對該語言進行貢獻。

Kotlin Android 模板

https://github.com/nekocode/Kotlin-Android-Template

Android 專案模板,使其非常容易設定穩定的 Kotlin 工作區,並快速引導你開發應用程式。

不可錯過的 Kotlin 資源列表

https://github.com/KotlinBy/awesome-kotlin

這是一個比較完整的 Kotlin 資源列表,包括各種實用連結、書籍、庫、框架和視訊等。該列表的組織結構非常好,kotlin.link 也提供了一個風格化的版本。

kotlin設計模式

https://github.com/dbacinski/Design-Patterns-In-Kotlin

DariuszBaciński 建立了一個 GitHub repo,其中有在 Kotlin 中實現的常見設計模式,也有用其他語言編寫的類似專案,包括 Java,Swift,Java 和 PHP,如果你是其中一項語言的使用者,可以用它們作為參考點。

二、視訊資源

Kotlin 介紹

https://www.youtube.com/watch?v=X1RVYt2QKQE

來自 Google I / O 2017 的演講,大會首次向人們介紹 Kotlin,並提出了改進工作流程的想法。它涵蓋了許多基礎知識,並展示了一些很酷的 Kotlin 技巧。

明日勝於今,我用 Kotlin

https://www.youtube.com/watch?v=fPzxfeDJDzY

Google I / O 2017 大會關於 Kotlin 的第二個演講。這個演講涵蓋了更多高階話題,如設計模式,最佳實踐和其他常見規則。 演講也揭示了在生產中使用 Kotlin 的意義,以及在工作中採用新興語言將面臨的挑戰。

Peter Sommerhoff 教你學 Kotlin

https://www.youtube.com/playlist?list=PLpg00ti3ApRweIhdOI4VCFFStx4uXC__u

這是一個免費的 Kotlin 課程,適合初學者,前面介紹了從變數到條件迴圈和函式的所有基礎知識,後面會深入到更高階的主題,如 Kotlin 中的物件導向以及像 lambda 表示式的功能程式設計。

使用 Kotlin&Gradle 更好地開發 Android

https://www.youtube.com/watch?v=_DaZQ374Chc

這個講座從 2016 年開始,它介紹了現實世界中的程式語言功能,你將瞭解到 Kotlin 是如何適應 Android 工作流程中存在的工具。

使用 Kotlin&Gradle 更好地開發 Android

https://www.youtube.com/watch?v=ZlQhmkp_jyk

一個 8 分鐘的濃縮教程,讓你快速瞭解 Kotlin 的主要功能,如變數宣告、Lambdas、擴充套件功能等等。

Jake Wharton:用 Kotlin 進行 Android 開發

https://www.youtube.com/watch?v=A2LukgT2mKc&t

關於 Kotlin 的介紹,演講向我們解釋了新語言是如何改進 Android 生態系統的,並展示了許多炫酷的方式,我們可以使用智慧的 Kotlin 語法來獲得優勢。

掃碼關注公眾號“偉大程式猿的誕生“,更多幹貨等著你~
掃碼關注公眾號“偉大程式猿的誕生“,更多幹貨等著你~
掃碼關注公眾號“偉大程式猿的誕生“,更多幹貨等著你~

公眾號回覆“資料獲取”,獲取更多幹貨哦~
公眾號回覆“資料獲取”,獲取更多幹貨哦~
公眾號回覆“資料獲取”,獲取更多幹貨哦~