Flutter開發技術與分享(二) —— Flutter 入門(一)

版本記錄

版本號 時間
V1.0 2019.11.03 星期日

前言

Flutter是谷歌的移動UI框架,可以快速在iOS和Android上構建高質量的原生用戶界面。 Flutter可以與現有的代碼一起工作。在全世界,Flutter正在被越來越多的開發者和組織使用,并且Flutter是完全免費、開源的。目前公司的部分模塊就是在使用Flutter進行開發。感興趣的可以看下面幾篇文章。
1. Flutter開發技術與分享(一) —— 基本概覽(一)

開始

首先看下主要內容

通過使用VS Code編寫跨平臺應用程序,深入研究Flutter框架,以在單個代碼庫中構建iOSAndroid應用程序。下面是翻譯文章的地址

然后看下寫作環境

寫作環境:Dart 2, Flutter 1.7, VS Code

自十年前iOSAndroid平臺風起云涌以來,跨平臺開發一直是整個移動開發領域的目標。 能夠為iOS和Android編寫一個應用程序的功能可以為您的公司和團隊節省大量時間和精力。

多年來,已經發布了用于跨平臺開發的各種工具,包括基于Web的工具(例如AdobePhoneGap),強大的框架(例如MicrosoftXamarin)以及更新的工具(例如FacebookReact Native)。 每個工具集都有其優缺點,并且在移動行業中獲得了不同程度的成功。

進入跨平臺領域的最新框架是GoogleFlutter。 Flutter在兩個平臺上均具有快速的開發周期,快速的UI呈現,獨特的UI設計以及本機應用程序性能。


Introduction to Flutter

Flutter應用程序是使用Dart編程語言編寫的,該語言最初也來自Google,現在是ECMA標準。 Dart與其他現代語言(例如KotlinSwift)具有許多相同的功能,并且可以轉編譯為JavaScript代碼。

作為跨平臺框架,Flutter最類似于React Native,因為Flutter允許響應式和聲明式編程風格。但是,與React Native不同,Flutter不需要使用Javascript橋接,這可以縮短應用程序的啟動時間和整體性能。 Dart通過使用Ahead-Of-TimeAOT編譯來實現此目的。

Dart的另一個獨特之處在于它還可以使用Just-In-Time or JIT編譯。 FlutterJIT編譯通過允許熱重裝(hot reload)功能在開發過程中刷新UI而無需全新的構建,從而改善了開發工作流程。

正如您將在本教程中看到的那樣,Flutter框架主要圍繞widgets的概念構建。在Flutter中,widgets不僅用于應用程序的視圖,而且還用于整個屏幕甚至應用程序本身。

除了跨平臺的iOS和Android開發之外,學習Flutter還可以讓您搶先開發Fuchsia平臺,Fuchsia平臺目前是Google開發的實驗性操作系統。

在本教程中,您將構建一個Flutter應用程序,該應用程序將查詢GitHub API中的GitHub組織中的團隊成員,并在可滾動列表中顯示團隊成員信息:

您可以同時使用iOS模擬器或Android模擬器來開發應用程序!

在構建應用程序時,您將了解有關Flutter的以下知識:

  • Setting up your development environment
  • Creating a new project
  • Hot reload
  • Importing files and packages
  • Using widgets and creating your own
  • Making network calls
  • Showing items in a list
  • Adding an app theme

順帶著你也會學習一點關于Dart的知識。


Setting up your development environment

Flutter開發可以在macOSLinuxWindows上完成。 盡管您可以將任何編輯器與Flutter工具鏈一起使用,但是有 IntelliJ IDEA,Android StudioVisual Studio CodeIDE插件可以簡化開發周期。 在本教程中,我們將使用VS Code

在此處here可以找到有關使用Flutter框架設置開發機器的說明?;静襟E因平臺而異,但大多數情況是:

  • 1) 下載適用于您開發計算機操作系統的安裝包,以獲取Flutter SDK的最新穩定版本
  • 2) 將安裝包解壓縮到所需位置
  • 3) 將flutter工具添加到您的路徑
  • 4) 運行flutter doctor命令,該命令將安裝Flutter框架(包括Dart)并提醒您任何缺少的依賴項
  • 5) 安裝缺少的依賴項
  • 6) 使用Flutter插件/擴展程序設置您的IDE
  • 7) 測試驅動一個應用

Flutter網站上提供的說明做得很好,可以讓您輕松地在所選平臺上設置開發環境。本教程的其余部分假定您已經為Flutter開發設置了VS Code,并且已經解決了flutter doctor發現的所有問題。

如果您使用的是Android Studio,那么您也應該能夠很好地遵循。您還需要運行iOS模擬器,Android模擬器,或者已設置預配置的iOS設備或Android設備進行開發。

注意:要在iOS模擬器或iOS設備上進行構建和測試,您需要使用已安裝Xcode的macOS。


Creating a new project

在安裝了Flutter擴展的VS Code中,通過選擇View ? Command Palette…或在macOS上單擊Cmd-Shift-P或在Linux或Windows上單擊Ctrl-Shift-P來打開命令面板。 在面板中輸入Flutter:New Project,然后按回車鍵。

輸入項目的名稱ghflutter,然后按回車鍵。 選擇一個文件夾來存儲項目,然后等待Flutter在VS Code中設置項目。 項目準備就緒后,將在編輯器中打開文件main.dart。

VS Code中,您會在左側看到一個面板,該面板顯示您的項目結構。 有適用于iOS和Android的文件夾,還有一個包含main.dartlib文件夾,并且具有適用于兩個平臺的代碼。 僅在本教程中,您將在lib文件夾中工作。

用以下內容替換main.dart中的代碼:

import 'package:flutter/material.dart';

void main() => runApp(GHFlutterApp());


class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GHFlutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('GHFlutter'),
        ),
        body: Center(
          child: Text('GHFlutter'),
        ),
      ),
    );
  }
}

頂部附近的main()函數對單個行函數使用=>運算符來運行該應用程序。 您有一個名為GHFlutterApp的應用程序類。

您在這里看到您的應用程序本身是一個StatelessWidget。 Flutter應用程序中的大多數實體都是無狀態或有狀態的widgets。 您可以覆蓋widgetsbuild()方法來創建應用widgets。 您正在使用MaterialApp widgets,該widgets提供了Material Design之后的應用所需的許多組件。

對于本入門教程,通過右鍵單擊,選擇Delete選項,然后確認刪除,從項目中刪除test文件夾中的測試文件widget_test.dart。

如果您使用的是macOS,請啟動iOS模擬器。 您也可以在macOS,Linux或Windows上使用Android模擬器。 如果iOS模擬器和Android模擬器都在運行,則可以使用VS Code窗口右下方的菜單在它們之間進行切換:

要構建和運行項目,您需要首先設置啟動配置。

通過單擊左側面板上的crossed bug圖標,切換到Debug View。

您會注意到,到目前為止,尚未定義任何配置。 單擊No Configuration以獲取下拉列表并選擇Add Configuration

VS Code將創建一個launch.json文件,其詳細信息如下:

注意:選擇Add Configuration項后,將自動為您生成此文件。 在本教程中,您無需修改它。

現在,您已經完成所有工作,可以通過按F5或選擇Debug ? Start Debugging或單擊綠色的播放圖標來構建和運行項目。 您會看到Debug Console已打開,并且如果在iOS上運行,則會看到用于構建項目的Xcode。 如果在Android上運行,則會看到Gradle被調用以進行構建。

這是在iOS模擬器中運行的應用程序:

下面,在Android模擬器中運行:

您看到的慢速模式banner表明該應用程序正在調試模式下運行。

您可以通過單擊VS Code窗口頂部工具欄右側的停止按鈕來停止正在運行的應用程序:

通過單擊VS Code左上方的圖標或選擇View ? Explorer,可以返回項目視圖。


Hot Reload

Flutter開發的最佳方面之一是能夠在進行更改時熱重新加載您的應用程序。 這類似于Android StudioInstant Run/Apply Changes。

構建并運行該應用程序,使其在模擬器或模擬器上運行:

現在,無需停止正在運行的應用程序,請將應用程序欄字符串更改為其他內容:

appBar: AppBar(
  title: Text('GHFlutter App'),
),

現在,單擊工具欄上的熱重載按鈕或直接保存main.dart文件:

一兩秒鐘之內,您應該會看到正在運行的應用程序中反映出的更改:

熱重載功能可能并不總是有效,官方文檔official docs可以很好地解釋無法使用的情況,但總體而言,在構建UI時可節省大量時間。


Importing a File

您將希望能夠從創建的其他類中導入代碼,而不是將所有Dart代碼都保存在單個main.dart文件中。 現在,您將看到一個導入字符串的示例,該示例在需要本地化面向用戶的字符串時會有所幫助。

右鍵單擊lib并選擇New File,在lib文件夾中創建一個名為strings.dart的文件:

將以下類添加到新文件中:

class Strings {
  static String appTitle = "GHFlutter";
}

將以下import添加到main.dart的頂部

import 'strings.dart';

更改widget以使用新的字符串類,以便GHFlutterApp類如下所示:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: Text(Strings.appTitle),
        ),
        body: Center(
          child: Text(Strings.appTitle),
        ),
      ),
    );
  }
}

按下F5鍵即可構建并運行該應用,您應該不會有任何變化,但是現在您正在使用字符串文件中的字符串。


Widgets

Flutter應用程序中的幾乎每個元素都是widget。 widget被設計為不可變的,因為使用不可變的widget有助于使應用程序UI保持輕便。

您將使用兩種基本類型的小部件:

  • Stateless - 無狀態:僅依賴于自己的配置信息的widget,例如圖像視圖中的靜態圖像。
  • Stateful - 有狀態:需要維護動態信息并通過與State對象進行交互來實現的信息。

無狀態小部件和有狀態widget都在Flutter應用程序中的每幀上重新繪制,不同之處在于,有狀態widget將其配置委托給State對象。

要開始制作自己的widget,請在main.dart底部創建一個新類:

class GHFlutter extends StatefulWidget {
  @override
  createState() => GHFlutterState();
}

您已經創建了StatefulWidget子類,并且您將覆蓋createState()方法以創建其狀態對象。 現在,在GHFlutter上方添加GHFlutterState類:

class GHFlutterState extends State<GHFlutter> {
}

GHFlutterState使用GHFlutter的參數擴展State。

制作widget時的主要任務是覆蓋將widget呈現到屏幕時調用的build()方法。

GHFlutterState中添加一個build()重寫:

@override
Widget build(BuildContext context) {
?    
}

填寫build()如下:

@override
Widget build(BuildContext context) {
  return Scaffold (
    appBar: AppBar(
      title: Text(Strings.appTitle),
    ),
    body: Text(Strings.appTitle),
  );
}

Scaffold是用于材料設計widgets的容器。 它充當widgets層次結構的根。 您已在Scaffold中添加了一個AppBar和一個body,每個都包含一個Text widget

更新GHFlutterApp,使其使用新的GHFlutter小部件作為其home屬性,而不是構建自己的支架:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      home: GHFlutter(),
    );
  }
}

構建并運行該應用程序,您將看到新的widget在起作用:

尚未發生太大變化,但是現在您可以設置以構建新的widget


Making Network Calls

之前,您已將strings.dart文件導入到項目中。 您可以類似地導入Flutter框架和Dart中包含的其他軟件包。

例如,您現在將使用框架中可用的包進行HTTP網絡調用,并將生成的響應JSON解析為Dart對象。 在main.dart頂部添加兩個新導入:

import 'dart:convert';
import 'package:http/http.dart' as http;

您會注意到http包不可用。 這是因為尚未將其添加到項目中。 導航到pubspec.yaml文件,然后在dependenciescupertino_icons:^ 0.1.2下添加以下內容:

  cupertino_icons: ^0.1.2

  # HTTP package
  http: ^0.12.0+2

注意:注意縮進。 保持 http 軟件包聲明的縮進與 cupertino_icons軟件包的縮進相同。

現在,當您保存pubspec.yaml文件時,VS Code中的Flutter擴展名將運行flutter pub get命令。 Flutter將獲得聲明的http軟件包,您的軟件包也將在main.dart中可用。

現在,您將在main.dart中看到有關當前未使用的導入的指示器。

Dart應用程序是單線程的,但是Dart提供了對在其他線程上運行代碼以及運行異步代碼的支持,這些異步代碼不會使用async / await模式阻止UI線程。

您將進行異步網絡調用以檢索GitHub團隊成員的列表。 在GHFlutterState的頂部添加一個空列表作為屬性,還添加一個屬性以容納文本樣式:

var _members = [];

final _biggerFont = const TextStyle(fontSize: 18.0);

名稱開頭的下劃線使該類的成員成為私有成員。

要進行異步HTTP調用,請向GHFlutterState添加方法_loadData()

_loadData() async {
  String dataURL = "https://api.github.com/orgs/raywenderlich/members";
  http.Response response = await http.get(dataURL);
  setState(() {
    _members = json.decode(response.body);
  });
}

您已經在_loadData()上添加了async關鍵字,以告知Dart它是異步的,并且還在http.get()調用上阻塞了await關鍵字。 您使用的dataUrl值設置為GitHub API端點,該端點檢索GitHub組織的成員。

HTTP調用完成后,您將向回調傳遞給setState(),該回調在UI線程上同步運行。 在這種情況下,您將解碼JSON響應并將其分配給_members列表。

initState()重寫添加到GHFlutterState,該狀態在初始化狀態時調用_loadData()

@override
void initState() {
  super.initState();

  _loadData();
}

Using a ListView

現在您已經有了Dart成員列表,您需要一種在UI列表中顯示它們的方法。 Dart提供了一個ListView widget,可讓您在列表中顯示數據。 ListView的行為類似于Android上的RecyclerView和iOS上的UITableView,在用戶滾動列表以實現平滑滾動性能時回收視圖。

_buildRow()方法添加到GHFlutterState中:

Widget _buildRow(int i) {
  return ListTile(
    title: Text("${_members[i]["login"]}", style: _biggerFont)
  );
}

您將返回一個ListTile widget,該widget顯示從ith成員的JSON解析的login值,并使用您之前創建的文本樣式。

更新GHFlutterState的構建方法,使其主體為ListView.builder

body: ListView.builder(
  padding: const EdgeInsets.all(16.0),
  itemCount: _members.length,
  itemBuilder: (BuildContext context, int position) {
    return _buildRow(position);
  }),

你已經添加padding,itemCount設置為成員的數量,并使用_buildRow()為給定的位置設置itemBuilder

您可以嘗試熱重載,但可能會收到“Full restart may be required”消息。 如果是這樣,請按F5鍵構建并運行該應用程序:

進行網絡通話,解析數據并在列表中顯示結果就是這么簡單!


Adding dividers

要將分隔符添加到列表中,您需要將item數量加倍,然后在列表中的位置為奇數時返回Divider widget。 如下更新GHFlutterState的構建方法:

body: ListView.builder(
  itemCount: _members.length * 2,
  itemBuilder: (BuildContext context, int position) {
    if (position.isOdd) return Divider();

    final index = position ~/ 2;
    
    return _buildRow(index);
  }),

確保不要錯過itemCount上的* 2。 有了分隔線后,您已經從構建器中刪除了padding。 在itemBuilder中,您要么返回Divider(),要么通過整數除法并使用_buildRow()來構建行項目來計算新索引。

嘗試熱重載,您應該在列表上看到分隔線:

要將padding重新添加到每一行中,您想在_buildRow()中使用Padding widget

Widget _buildRow(int i) {
  return Padding(
    padding: const EdgeInsets.all(16.0),
    child: ListTile(
      title: Text("${_members[i]["login"]}", style: _biggerFont)
    )
  );
}

ListTile現在是padding widget的子widget。 熱重新加載以查看行上的padding,而不是分隔線上的padding。


Parsing to Custom Types

在上一節中,JSON解析器將JSON響應中的每個成員作為Dart Map類型添加到_members列表中,相當于Kotlin中的MapSwift中的Dictionary

但是,您還希望能夠使用自定義類型。

main.dart文件中添加一個新的Member類型:

class Member {
  final String login;

  Member(this.login) {
    if (login == null) {
      throw ArgumentError("login of Member cannot be null. "
          "Received: '$login'");
    }
  }
}

成員具有login屬性和一個構造函數,如果登錄值為null,則該構造函數將拋出錯誤。

更新GHFlutterState中的_members聲明,以便它是Member對象的列表:

var _members = <Member>[];

更新_buildRow()以在Member對象上使用login屬性,而不是使用映射上的login鍵:

title: Text("${_members[i].login}", style: _biggerFont)

現在,更新發送到_loadData()中的setState()的回調,以將解碼后的映射轉換為Member對象并將其添加到成員列表中:

setState(() {
  final membersJSON = json.decode(response.body);

  for (var memberJSON in membersJSON) {
    final member = Member(memberJSON["login"]);
    _members.add(member);
  }
});

如果嘗試進行熱重裝,您可能會看到一個錯誤,但是停止并按F5鍵來構建和運行該應用,您應該會看到與以前相同的屏幕,除了現在使用新的Member類。


Downloading Images with NetworkImage

來自GitHub的每個成員都有其頭像的URL。 現在,您將該頭像添加到Member類中,并在應用程序中顯示頭像。

更新Member類以添加一個avatarUrl屬性,該屬性不能為null

class Member {
  final String login;
  final String avatarUrl;

  Member(this.login, this.avatarUrl) {
    if (login == null) {
      throw ArgumentError("login of Member cannot be null. "
          "Received: '$login'");
    }
    if (avatarUrl == null) {
      throw ArgumentError("avatarUrl of Member cannot be null. "
          "Received: '$avatarUrl'");
    }
  }
}

使用NetworkImageCircleAvatar widget更新_buildRow()以顯示頭像:

Widget _buildRow(int i) {
  return Padding(
    padding: const EdgeInsets.all(16.0),
    child: ListTile(
      title: Text("${_members[i].login}", style: _biggerFont),
      leading: CircleAvatar(
        backgroundColor: Colors.green,
        backgroundImage: NetworkImage(_members[i].avatarUrl)
      ),
    )
  );
}

通過將頭像設置為ListTileleading屬性,它將在行內標題之前顯示。 您還使用Colors類在圖片上設置了背景色。

現在更新_loadData()以在創建新Member時使用映射中的“ avatar_url”值:

final member = Member(memberJSON["login"], memberJSON["avatar_url"]);

使用F5停止,構建和運行該應用程序。 您會在每一行中看到您的成員頭像:


Cleaning the Code

現在,您的大多數代碼都位于main.dart文件中。 為了使代碼更簡潔,您可以重構已添加到文件中的widget和其他類。

lib文件夾中創建名為member.dartghflutter.dart的文件。 將Member類移至member.dart,并將GHFlutterStateGHFlutter類移至ghflutter.dart。

您在member.dart中不需要任何import語句,但是ghflutter.dart中的導入應如下所示:

import 'dart:convert';
import 'package:http/http.dart' as http;

import 'package:flutter/material.dart';

import 'member.dart';
import 'strings.dart'; 

您還需要更新main.dart中的導入,以便整個文件包含以下內容:

import 'package:flutter/material.dart';

import 'ghflutter.dart';
import 'strings.dart';

void main() => runApp(GHFlutterApp());


class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      home: GHFlutter(),
    );
  }
}  

按下F5來構建和運行該應用程序,您應該看不到任何更改,但是代碼現在更簡潔了。


Adding a Theme

您可以通過將theme屬性添加到您在main.dart中創建的MaterialApp中,輕松地將主題添加到應用中:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      theme: ThemeData(primaryColor: Colors.green.shade800), 
      home: GHFlutter(),
    );
  }
}  

您將綠色用作主題的“材料設計”顏色值。

按下F5來構建和運行應用程序,以查看新的主題:

大多數應用程序屏幕截圖均來自Android模擬器。 您還可以在iOS模擬器中運行最終的主題應用程序:

這就是我所說的跨平臺!

有關FlutterDart的知識還有很多。 最好的起點是:

  • flutter.dev上的Flutter主頁。 您會發現很多很棒的文檔和其他信息。
  • 在此處here查看可用的widgets
  • 這里here有一個很好的指南供Android開發人員過渡到使用Flutter。
  • 適用于React Native開發人員的類似指南在這里here。

后記

本篇主要講述了Flutter 入門,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容