還在學(xué)iOS?是時(shí)候?qū)W習(xí)Flutter了(二)

概述

本文承接上文,是Flutter For iOS 的第二篇文章,通過閱讀本文你將獲取如下信息:

  • 線程和異步
  • 項(xiàng)目結(jié)構(gòu)與本地化
  • 視圖控制器
  • 布局
  • 手勢(shì)
  • 表單
  • 列表
  • 其他

線程和異步

如何寫異步代碼

Dart擁有單線程執(zhí)行模型,同時(shí)也支Isolate (一種將Dart代碼執(zhí)行在另一個(gè)線程的方式)、事件循環(huán)和異步編程。除非你創(chuàng)建一個(gè)Isolate ,你的Dart代碼將一直在主UI線程中執(zhí)行,并由事件循環(huán)驅(qū)動(dòng)。Flutter的事件循環(huán)相當(dāng)于iOS中的主循環(huán),也就是說Looper 綁定在主線程上。

Dart的單線程模型并不意味著你必須將一切代碼作為一個(gè)導(dǎo)致UI卡頓的阻塞塊來執(zhí)行。相反,你可以使用Dart提供的異步功能比如說:async/awiat 來執(zhí)行異步任務(wù)。

比如說,你可以使用asyn/await執(zhí)行網(wǎng)絡(luò)代碼和繁重的工作而避免UI卡頓。

image

一旦網(wǎng)絡(luò)請(qǐng)求結(jié)束,通過調(diào)用setState()更新UI,觸發(fā)當(dāng)前widget的子樹和更新數(shù)據(jù)。

下面例子異步加載數(shù)據(jù)并展示在ListViews上:

image

參考下一節(jié)了解如何在后臺(tái)線程執(zhí)行任務(wù),與iOS有何不同。

如何將任務(wù)放到后臺(tái)線程

由于Flutter的單線程模型和事件循環(huán),你不用擔(dān)心線程管理或者開啟后臺(tái)線程。你可以放心的使用async/await方法執(zhí)行I/O操作,比如訪問磁環(huán)或者請(qǐng)求網(wǎng)絡(luò)。另一方面,如何你想執(zhí)行復(fù)雜的計(jì)算而使CPU持續(xù)的處于繁忙狀態(tài),你可以將任務(wù)已到Isolate而避免阻塞事件循環(huán)。

對(duì)于iOS操作,將方法聲明為async方法,使用await等待耗時(shí)任務(wù)完成。

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

這是對(duì)常的I/O操作如網(wǎng)絡(luò)請(qǐng)求,訪問數(shù)據(jù)庫的常規(guī)操作。

但是,當(dāng)你處理大量數(shù)據(jù)的時(shí)候這仍然可能會(huì)導(dǎo)致UI掛起。在Flutter中,使用Isolate 來使用CPU多核的優(yōu)勢(shì)來執(zhí)行耗時(shí)任務(wù)或者計(jì)算密集型任務(wù)。

Isolates 是分離線程,它不和主線程共享任何堆內(nèi)存,這也就意味著,你不能訪問主線程中的變臉,或者直接調(diào)用setState()更新主線程。Isolates正如其名,不能共享內(nèi)存。

下面代碼展示了一個(gè)簡(jiǎn)單的isolate, 如何將數(shù)據(jù)返回到主線程并更新UI的。

image

上面代碼中,dataLoader()Isolate,它在一個(gè)獨(dú)立的線程中執(zhí)行。在這個(gè)isolate中你可以執(zhí)行CPU密集型任務(wù)如解析JSON,或者執(zhí)行浮躁的數(shù)學(xué)計(jì)算任務(wù),如加密或者信號(hào)處理。

你可以執(zhí)行完整代碼,如下:

image

如何發(fā)生網(wǎng)絡(luò)請(qǐng)求

在Flutter中使用流行的第三方庫http package 來請(qǐng)求網(wǎng)絡(luò)是非常簡(jiǎn)單的。它抽象了大量的本需要你自己實(shí)現(xiàn)的操作,使得發(fā)送請(qǐng)求非常簡(jiǎn)單。

為了使用http這個(gè)框架,你需要在pubspec.yaml中增加依賴。

dependencies:
  ...
  http: ^0.11.3+16

為了發(fā)起網(wǎng)絡(luò)請(qǐng)求,在async方法http.get() 前添加await。

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

如何展示耗時(shí)任務(wù)的進(jìn)度

在iOS中,當(dāng)在后臺(tái)執(zhí)行一個(gè)耗時(shí)任務(wù)的時(shí)候,你通過會(huì)使用UIProgressView展示進(jìn)度。

在Flutter中,使用ProgressIndicator 組件。通過給它傳遞一個(gè)布爾標(biāo)識(shí)來控制它的展示,告訴Flutter去更新它的狀態(tài)在耗時(shí)任務(wù)執(zhí)行之前和執(zhí)行結(jié)束之后隱藏掉它。

在下面的例子中,build方法被分割為三個(gè)不同方法。如果showLoadingDialog()是true,那就渲染ProgressIndicator 否則使用網(wǎng)絡(luò)返回的數(shù)據(jù)渲染ListView。

image

項(xiàng)目結(jié)構(gòu)、本地化、依賴和資源管理

如何在Flutter中管理圖片,如何放置多種分辨率的圖片

與iOS將圖片和資源作為不同的類型來處理不同的是Flutter中只有一種assets。iOS中資源被放在Image.xcassert中文件中,而Flutter中放在assets文件中。與iOS一樣,assets是許多類型的文件,不僅僅是圖片,比如說你可以將json文件放到my-assets文件夾中。

my-assets/data.json

pubspec.yaml文件中聲明:

assets:
    - my-assets/data.json

然后就可以在代碼中使用AssetBunlde訪問:

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('my-assets/data.json');
}

對(duì)于圖片,F(xiàn)lutter和iOS的格式一樣,圖片可以是1倍圖,2倍圖,3倍圖或者其他任何倍數(shù)。這些所謂的 devicePixelRatio 表示的是物理像素到單個(gè)邏輯像素的比率。

Assets可以被放到任何類型的文件夾中,F(xiàn)lutter中沒有事先預(yù)定義文件的結(jié)構(gòu)。在pubSpec.yaml文件中聲明assets,然后Flutter就能識(shí)別出來。

比如說:將my_icon.png放置到Flutter項(xiàng)目中,你可能把存儲(chǔ)的文件夾叫作images。把相關(guān)系數(shù)的圖片放在不同的子文件家中,如下:a

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接下來在pubspec.yaml中聲明圖片

assets:
    - images/my_icon.png

你現(xiàn)在就可以使用AssetImage返回圖片

return AssetImage("images/a_dot_burr.jpeg");

或者直接使用Image組件

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

更多細(xì)節(jié)參考Adding Assets and Images in Flutter。

如何存放字符串,如何管理本地化

iOS中,我們使用Localizable.strings文件管理本地化字符串,而Flutter中沒有專門的模塊處理本地化字符串,所以最好的辦法就是將字符串統(tǒng)一放到一個(gè)類中,以靜態(tài)字段的形式存儲(chǔ)。如下:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

訪問方式如下:

Text(Strings.welcomeMessage)

默認(rèn)情況下,F(xiàn)lutter只支持英文字符串,如果你想支持其他語言,可以通過引入flutter_localizations庫。 同時(shí)你需要將Dart的intl包以便支持 i10n 機(jī)制,比如日期/時(shí)間格式化。

dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

為了使用flutter_localizations ,需要在App widget上指定 localizationsDelegatessupportedLocales 屬性。

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
 localizationsDelegates: [
   // Add app-specific localization delegate[s] here
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)

代理中包含了實(shí)際的本地化值,supportedLocales定義了要支持那些語言的本地化。上面的例子使用的是MaterialApp, 它既有針對(duì)基本W(wǎng)idget的本地化值GlobalWidgetsLocalizations,也有針對(duì)Material widget的MaterialWidgetsLocalizations本地化。如果你的App使用的是WidgetApp,那么后者就不需要了。值得注意的是這兩個(gè)代理都包含默認(rèn)值,但如果你想讓你的App本地化,你扔需要提供一個(gè)或者多個(gè)代理作為你的App本地化副本。

當(dāng)初始化完成的時(shí)候,WidgetsApp或者MaterialApp使用你指定的代理為你創(chuàng)建了一個(gè)Localizationswidget。你可從LocalizationsWidget中隨時(shí)訪問當(dāng)前設(shè)備的本地化信息,或者使用window.locale

為了訪問本地化資源,使用Localizations.of()方法訪問有給定的delegate提供的特有的本地化類。使用intl_translation取出翻譯副本到 arb 文件中。將它們引入App中,并用intl來使用它們。

更多國際化和本地化的內(nèi)容參考: internationalization guide,它包含了不使用intl示例代碼。

需要注意的是:Flutter1.0 beta2 之前 fullter中定義的資源文件不能被原生訪問,同時(shí)原生定義的資源不能被flutter訪問,因?yàn)樗鼈兇鎯?chǔ)在不能的文件目錄下。

如何管理依賴

在iOS中,我們將依賴添加到Podfile文件中,F(xiàn)lutter使用的是Dart語言構(gòu)建的系統(tǒng)和Pub包管理器操作依賴。這些工具將原生 Android 和 iOS 包裝應(yīng)用程序的構(gòu)建委派給相應(yīng)的構(gòu)建系統(tǒng)。

如果在你的Flutter項(xiàng)目中iOS目錄下包含Podfile,只需要使用它添加iOS原生的依賴。使用 pubspec.yaml 聲明Flutter 中的外部依賴。 Pub網(wǎng)站可以找到一些比較好用的第三方依賴。

視圖控制器

Flutter中與ViewControllers相等的元素是什么?

在iOS中,ViewController表示用戶界面的一部分,通常表示一個(gè)屏幕或者部分屏幕。多個(gè)ViewController組合在一起構(gòu)造復(fù)雜的用戶界面,并幫助你規(guī)整應(yīng)用的UI部分。在Flutter中,這項(xiàng)工作落在了Widget頭上,正如導(dǎo)航那一個(gè)章節(jié)提到的,屏幕由Widget所表示,因"一切都是Widget"。使用Navigator在不同的路由間切換表示不同的屏幕或者頁面或者表示不同的狀態(tài)或者渲染相同的數(shù)據(jù)。

如何監(jiān)聽iOS的生命周期事件

在iOS中,你可以重寫ViewController中的方法來捕獲視圖的生命周期,或者在AppDelegate中注冊(cè)生命周期的回調(diào)。在Flutter中沒有這兩個(gè)概念,但是我們可以通過hookWidgetsBinding并在didChangeAppLifecycleState()方法中監(jiān)聽生命周期事件。

能夠監(jiān)聽到的生命周期事件如下:

  • Inactive — 應(yīng)用程序處于不活躍狀態(tài),不能相應(yīng)用戶輸入。該事件只在iOS中有效。
  • paused — 應(yīng)用程序當(dāng)前不可用,不響應(yīng)用戶輸入,但是還在后臺(tái)運(yùn)行。
  • resumed — 應(yīng)用程序可用,并能響應(yīng)用戶輸入。
  • suspending — 應(yīng)用程序暫時(shí)被掛起。該事件只在Android系統(tǒng)上有效。

更多細(xì)節(jié)參考:AppLifecycleStatus documentation。

布局

Flutter中的UITableViewUICollectionView

Flutter中使用ListView實(shí)現(xiàn)iOS中的UITableViewUICollectionView。實(shí)現(xiàn)代碼如下:

image

如何知道那個(gè)cell被點(diǎn)擊

在iOS中,通過實(shí)現(xiàn) tableView:didSelectRowAtIndexPath:方法來相應(yīng)cell的點(diǎn)擊事件,在Flutter中,使用所包含的widget本身提供的事件來處理相應(yīng)。

image

如何動(dòng)態(tài)更新ListView

在iOS中,我們使用reloadData來刷新表格視圖。

在Flutter中,如果更新setState()中的小部件列表,你會(huì)發(fā)現(xiàn)列表數(shù)據(jù)沒有發(fā)生變化。這是因?yàn)楫?dāng)調(diào)用setState()時(shí),F(xiàn)lutter呈現(xiàn)引擎會(huì)查看widget樹以查看是否有任何更改。當(dāng)它到達(dá)ListView時(shí),它執(zhí)行==檢查,并確定兩個(gè)ListView是相同的。沒有任何改變,因此不需要更新。

在setState()方法內(nèi)創(chuàng)建一個(gè)新List是更新ListView的一個(gè)簡(jiǎn)單的方法。并將舊列表中的數(shù)據(jù)復(fù)制到新列表中。雖然這種方法很簡(jiǎn)單,但不建議用于大型數(shù)據(jù)集,如下一個(gè)示例所示。


image

我們推薦使用ListView.Builder來構(gòu)建列表,它比較高效。當(dāng)你的列表包含大量數(shù)據(jù)的列表時(shí),此方法非常有用。

image

與創(chuàng)建一個(gè)ListView不同的是,創(chuàng)建ListView.builder 攜帶兩個(gè)參數(shù):列表的初始長度和ItemBuilder方法。

ItemBuilder方法和iOS中的table或者collection的cellForItemAt代理相似,一樣的攜帶一個(gè)位置,并返回該位置需要渲染的cell。

最后也是最重要的,onTap方法并沒有重新創(chuàng)建一個(gè)list,而是.add了一個(gè)Widget。

如何使用類似ScrollView的功能

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

手勢(shì)檢測(cè)和觸摸事件處理

如何向widget添加一個(gè)事件監(jiān)聽

如果widget支持事件處理,如RaisedButton,可以直接將相應(yīng)方法傳遞給對(duì)應(yīng)的屬性,如RaisedButton的onPressed。

Widget build(BuildContext context) {
  return RaisedButton(
    onPressed: () {
      print("click");
    },
    child: Text("Button"),
  );
}

如果widget不支持事件處理,可以使用GestureDetector包裹一下,然后給onTap屬性傳遞一個(gè)方法。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Sample App'),
    ),
    body: Center(
      child: GestureDetector(
        child: FlutterLogo(
          size: 200,
        ),
        onTap: () {
          print('taped');
        },
      ),
    ));
}

如何處理widget上的其他類型的事件

我們可以使用 GestureDetector 來實(shí)現(xiàn)如下事件的監(jiān)聽:

  • 單擊
    • onTapDown — 按下手勢(shì)事件
    • onTapUp — 抬起事件
    • onTap — 點(diǎn)擊事件
    • onTapCancel — 取消點(diǎn)擊事件,onTapDown發(fā)生,但onTap沒有發(fā)生。
  • 雙擊
    • onDoubleTap — 雙擊事件
  • 長按
    • onLongPress — 長按事件
  • 垂直拖動(dòng)
    • onVerticalDragStart —開始垂直移動(dòng)
    • onVerticalDragUpdate — 垂直移動(dòng)進(jìn)行中。
    • onVerticalDragEnd — 垂直移動(dòng)結(jié)束。
  • 水平拖動(dòng)
    • onHorizontalDragStart — 開始水平移動(dòng)。
    • onHorizontalDragUpdate — 水平移動(dòng)進(jìn)行中。
    • onHorizontalDragEnd — 水平移動(dòng)結(jié)束。

下面代碼展示了使用 GestureDetector 實(shí)現(xiàn)雙擊事件:

image

運(yùn)行效果:

image

主題和文本

如何為應(yīng)用程序設(shè)置主題

Flutter提供了一套完美符合Material Design的主題,它幫你處理了大多數(shù)需要你自己處理的樣式和主題。

為了在你的App中充分發(fā)揮Material組件的優(yōu)勢(shì),在頂層組件上聲明MaterialApp,作為你的應(yīng)用的入口。MaterialApp 是一個(gè)便利的組件,它包含了許多App通常需要的Materail Desigin風(fēng)格的組件。它通過由給WidgetsApp增加MD功能實(shí)現(xiàn)的。

同時(shí) Flutter 足夠地靈活和富有表現(xiàn)力來實(shí)現(xiàn)任何其他的設(shè)計(jì)語言。在 iOS 上,你可以用 Cupertino library 來制作遵守 Human Interface Guidelines 的界面。查看這些 widget 的集合,請(qǐng)參閱 Cupertino widgets gallery。

你也可以在你的 App 中使用 WidgetApp,它提供了許多相似的功能,但不如 MaterialApp那樣豐富。

對(duì)任何子組件定義顏色和樣式,可以給 MaterialApp widget 傳遞一個(gè) ThemeData 對(duì)象。舉個(gè)例子,在下面的代碼中,primary swatch 被設(shè)置為藍(lán)色,并且文字的選中顏色是紅色:

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}

如何在Text widget上使用自定義字體

在 iOS 中,你在項(xiàng)目中引入任意的 ttf 文件,并在 info.plist 中設(shè)置引用。在 Flutter 中,在文件夾中放置字體文件,并在 pubspec.yaml 中引用它,就像添加圖片那樣。

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

然后在你的 Text widget 中指定字體:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何設(shè)置Text widget的樣式

除了字體以外,你也可以給 Text widget 的樣式元素設(shè)置自定義值。Text widget 接受一個(gè) TextStyle 對(duì)象,你可以指定許多參數(shù),如下:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

表單輸入

表單在Flutter中如何工作的,如何取回用戶輸入的值

在iOS中,我們通常在用戶提交的時(shí)候獲取組件上的內(nèi)容,對(duì)于具有使用獨(dú)立狀態(tài)的不可變組件的Flutter來講,你可能會(huì)好奇如何獲取用戶輸入內(nèi)容。

對(duì)于表單操作而言,與其他功能一樣也是通過特定的Widget實(shí)現(xiàn)的。通過使用 TextField或者TextFormField 可以通過 TextEditingController 取回輸入內(nèi)容。

示例代碼如下:

image

運(yùn)行效果

image

更多信息參考: Flutter CookbookRetrieve the value of a text field

如何實(shí)現(xiàn)類似文本輸入框占位符的功能

通過給decoration屬性傳遞一個(gè)InputDecoration對(duì)象來給TextField實(shí)現(xiàn)占位符的功能。

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  ),
)

如何展示驗(yàn)收錯(cuò)誤信息

與上面代碼一樣,只不過是再添加一個(gè)errorText字段,通過state控制錯(cuò)誤信息的提示。

示例代碼如下:


image

運(yùn)行效果:


image

與硬件、第三方服務(wù)和平臺(tái)的交互

如何與平臺(tái)和平臺(tái)原生代碼交互

Flutter不是在直接在平臺(tái)下運(yùn)行代碼的,相反,由Dart語言構(gòu)建的FlutterApp在設(shè)備本機(jī)運(yùn)行,"回避"平臺(tái)提供的SDK。比如說:在Dart中發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,它是直接在Dart上下文中執(zhí)行的,而不適用我們?cè)趯懺鶤pp的時(shí)候所使用的Android或者iOSAPI。我們的FlutterApp仍然被原生app的ViewController當(dāng)做一個(gè)View所持有,但我們不用直接訪問ViewController或者原生框架。

這并不意味著Flutter應(yīng)用不能與原生API或者其他你寫的原生代碼交互。Flutter提供了 platform channels,它可以與持有你Flutter視圖的VIewController通信或者交換數(shù)據(jù)。platform channels 本質(zhì)上是一個(gè)異步通信機(jī)制,橋接了Dart代碼和其宿主ViewController,iOS框架。比如說。你可以用platform channels執(zhí)行一個(gè)原生的函數(shù),或者是從設(shè)備的傳感器中獲取數(shù)據(jù)。

除了直接使用platform channels之外,你還可以使用一系列預(yù)先制作好的 plugins。例如,你可以直接使用插件來訪問相機(jī)膠卷或是設(shè)備的攝像頭,而不必編寫你自己的集成層代碼。你可以在 Pub 上找到插件,這是一個(gè) Dart 和 Flutter 的開源包倉庫。其中一些包可能會(huì)支持集成 iOS 或 Android,或兩者均可。

如果你在 Pub 上找不到符合你需求的插件,你可以自己編寫 ,并且發(fā)布在 Pub 上。

如何訪問GPS傳感器

使用 geolocator

如何訪問相機(jī)

使用 image_picker

如何使用FaceBook登陸

使用 flutter_facebook_login

如何使用Firebase

大多數(shù) Firebase 特性被 first party plugins 包含了。這些第一方插件由 Flutter 團(tuán)隊(duì)維護(hù):

如何創(chuàng)建原生集成層代碼

如果有一些 Flutter 和社區(qū)插件遺漏的平臺(tái)相關(guān)的特性,可以根據(jù) developing packages and plugins 頁面構(gòu)建自己的插件。
Flutter 的插件結(jié)構(gòu),簡(jiǎn)要來說,就像 Android 中的 Event bus。你發(fā)送一個(gè)消息,并讓接受者處理并反饋結(jié)果給你。在這種情況下,接受者就是在 Android 或 iOS 上的原生代碼。

數(shù)據(jù)庫和本地存儲(chǔ)

如何在Flutter中使用UserDefaults

在iOS中,我們可以使用UserDefaults 來存儲(chǔ)鍵值對(duì)集合,在Flutter中,可以使用 Shared Preferences plugin插件來顯示類似的功能。 這個(gè)插件包裝了UserDefaults和Android 上的 SharedPreferences

Flutter中和Coredata相等的功能。

可以使用 SQFlite 插件實(shí)現(xiàn)iOS中CoreData相關(guān)的功能。

通知

如何設(shè)置推送通知

在iOS,你需要在開發(fā)者網(wǎng)站上注冊(cè)app以便獲取推送權(quán)限。在Flutter中使用firebase_messaging 插件可以實(shí)現(xiàn)推送。
更多關(guān)于使用Firebase Cloud Messaging API的文檔請(qǐng)參考: firebase_messaging

參考

本文主要參考Flutter官方文檔,F(xiàn)lutter中文網(wǎng)。
由于排版原因,文中我使用了圖片的形式展示代碼,如果你需要源碼,可以關(guān)注我的公眾號(hào),回復(fù)關(guān)鍵字"flutter"獲取相關(guān)代碼。

最后編輯于
?著作權(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)容