Flutter實戰一Flutter聊天應用(一)
不知不覺,進階的教程已經寫了幾十篇了,通過前面的學習,大家已經打下了良好的基礎,接下來我們就開始進行專案實戰吧!
我們現在要寫一個叫“談天說地”的應用程式,這是一個簡單、可擴充套件的聊天應用程式,能實時顯示資訊,使用者可以輸入文字資訊,也可以通過按返回鍵或傳送圖示傳送,還可以在iOS和Android裝置上執行。
首先我們要在IntelliJ編輯器中啟動一個新的Flutter專案:
啟動IntelliJ IDEA。
選擇
Create New Project > Flutter
,如果專案已經開啟,則選擇File > New > Project... > Flutter
/Users/obiwan/flutter
。輸入或瀏覽到Flutter SDK目錄,然後單擊Next。
命名您的專案,例如
talk_casually
。推薦使用全小寫字母命名,並使用下劃線字元作為分隔符,第一個字元必須是一個字母。預設儲存位置為$HOME/IdeaProjects/<project_name>
,但如果有需要,您也可以指定不同的目錄。按Finish以建立Flutter專案
現在我們修改一下預設的示例應用程式,新增的第一個元素是一個簡單的app bar
,用於顯示應用程式的靜態標題。隨著這個專案的後續進展,我們將逐步向應用程式新增更多響應和狀態的UI元素。
main.dart
檔案位於Flutter專案中的lib
目錄下,幷包含啟動執行應用程式的main()
函式。
main()
和runApp()
函式定義與預設應用程式中的相同,runApp()
函式作為引數,它是一個Widget
,Flutter框架在執行時展開並顯示在應用程式的螢幕上。由於應用程式在UI中使用質感設計元素,因此建立一個新的MaterialApp
物件並將其傳遞給runApp()
函式,這個控制元件是我們應用程式控制元件樹的根。
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
title: '談天說地' ,
home: new Scaffold(
appBar: new AppBar(
title: new Text('談天說地'),
)
)
));
}
要指定使用者在應用程式中看到的預設螢幕,需要在MaterialApp
定義中設定home
引數,home
引數引用了一個定義此應用程式的主UI的控制元件。該控制元件由一個Scaffold
控制元件組成,它具有一個簡單的AppBar
作為其子控制元件。
為了給互動式元件打下基礎,我們將簡單的應用程式分解為兩個不同的控制元件子類:一個永遠不會更改的根級別的TalkcasuallyApp
控制元件,以及一個可在訊息傳送和內部狀態更改時重建的子類ChatScreen
控制元件。現在,這兩個類都可以擴充套件StatelessWidget
,之後,我們將調整ChatScreen
來管理狀態。
import 'package:flutter/material.dart';
void main() {
runApp(new TalkcasuallyApp());
}
class TalkcasuallyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: '談天說地',
home: new ChatScreen(),
);
}
}
class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('談天說地'),
)
);
}
}
上面程式碼中的新控制元件有一些共同點:它們是佈局控制元件。他們的角色是執行佈局任務,比如錨定、對齊和分發其他控制元件。
在這裡要插一句話。隨著您繼續進行更改並優化應用程式的UI,您可以快速檢視結果,而不需要重新啟動完整的應用程式。使用Flutter的熱重新載入功能將更新的原始檔注入正在執行的Dart虛擬機器(Dart Virtual Machine)並重新整理UI。熱過載是實驗、原型設計和迭代的強大工具。需要注意的是,在IntelliJ IDEA中,熱過載按鈕是一個黃色閃電圖示。
Flutter框架提供了一個名為TextField的質感設計控制元件,它是一個有狀態的控制元件,具有用於自定義輸入欄位行為的屬性。作為該專案的第一個有狀態控制元件,它需要一些修改才能管理內部狀態更改。
在Flutter中,如果要在視窗控制元件中視覺化呈現狀態資料,則應將此資料封裝在State
物件中。然後,您可以將State
物件與擴充套件StatefulWidget
類的視窗控制元件相關聯。
以下程式碼片段顯示瞭如何開始在main.dart
檔案中定義一個類,用於新增互動式文字輸入欄位。首先,我們將ChatScreen
類更改為子類StatefulWidget
而不是StatelessWidget
。然後,我們將定義一個建立State物件的ChatScreenState
類。
覆蓋createState()
方法,以附加ChatScreenState
類,我們將使用新類來構建有狀態的TextField
控制元件。在build()
方法之上新增一行以定義ChatScreenState
類:
class ChatScreen extends StatefulWidget {
@override
State createState() => new ChatScreenState();
}
class ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('談天說地'),
)
);
}
}
現在,ChatScreenState
的build()
方法應該包括以前在控制元件樹的ChatScreen
部分中的所有視窗控制元件。當框架呼叫build()
方法來重新整理UI時,它可以使用它的子視窗控制元件樹來重建ChatScreenState
。
當你對一個類或方法有疑問時,檢視Flutter框架API的原始碼定義對我們是很有用,可以更好地瞭解幕後情況。您可以通過選擇一個類或方法名稱,然後右鍵單擊並選擇“Go to Declaration”選項,從IntelliJ的編輯器面板輕鬆完成此操作。根據作業系統的不同,您也可以點選鍵盤上的命令或控制按鈕。這是一個很好的習慣呢!
現在,我們的應用程式有能力管理狀態,您可以使用輸入欄位和傳送按鈕構建ChatScreenState
類。要管理與文字欄位的互動,需要使用TextEditingController
物件。您將使用它來讀取輸入欄位的內容,並在傳送訊息後清除該欄位。新增一行程式碼到ChatScreenState
類定義中以建立此物件。
class ChatScreenState extends State<ChatScreen> {
final TextEditingController _textController = new TextEditingController();
//...
以下程式碼片段顯示瞭如何定義一個名為_buildTextComposer()
的私有方法,該方法使用已配置的TextField
控制元件返回Container
控制元件。
從Container
控制元件開始,在螢幕的邊緣和輸入欄位的每一邊之間增加一個水平邊距。這裡的單位是根據裝置的畫素比例將邏輯畫素轉換為特定數量的物理畫素。比如iOS的points
或Android的density-independent pixels
等術語。
新增一個TextField
視窗控制元件,並按如下方式進行配置,以管理使用者互動:
要控制文字欄位的內容,我們將向
TextField
建構函式提供一個TextEditingController
,該控制器也可用於清除該欄位或讀取其值。要在使用者提交訊息時通知,需要使用
onSubmitted
引數提供一個私有回撥方法_handleSubmitted()
。現在這種方法只會清除該欄位,稍後我們將新增更多的程式碼來發送訊息。
class ChatScreenState extends State<ChatScreen> {
final TextEditingController _textController = new TextEditingController();
void _handleSubmitted(String text) {
_textController.clear();
}
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '傳送訊息'),
)
);
}
//...
現在,告訴應用程式如何顯示文字輸入控制元件,在ChatScreenState
類的build()
方法中,將一個名為_buildTextComposer
的私有方法附加到body
屬性。_buildTextComposer
方法返回一個封裝文字輸入欄位的控制元件。
class ChatScreenState extends State<ChatScreen> {
//...
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('談天說地'),
),
body: _buildTextComposer()
);
}
}
這裡要特別注意一點,從無狀態到有狀態控制元件的更改需要重新啟動應用程式。
接下來,我們將在文字欄位的右側新增一個“傳送”按鈕,由於我們要顯示與輸入欄位相鄰的按鈕,因此我們將使用Row
控制元件作為父項。然後將TextField
控制元件包裝在Flexible
控制元件中,這將使Row
自動將文字欄位的大小用於使用按鈕未使用的剩餘空間。
class ChatScreenState extends State<ChatScreen> {
//...
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget> [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '傳送訊息'),
)
),
]
)
);
}
//...
}
我們現在可以建立一個IconButton
控制元件,顯示傳送圖示。在icon
屬性中,使用Icons.send
常量建立一個新的Icon
例項。此常數表示您的控制元件使用質感圖示庫提供的“傳送”圖示。
將您的IconButton
控制元件放在另一個Container
父視窗控制元件中。這樣我們就可以自定義按鈕的邊距間距,使其在輸入欄位旁邊更好看。對於onPressed
屬性,使用匿名函式也呼叫_handleSubmitted()
方法,並使用_textController
傳遞訊息的內容。
class ChatScreenState extends State<ChatScreen> {
//...
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget> [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '傳送訊息'),
)
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text)),
)
]
)
);
}
//...
}
這裡要解釋一下,在Dart語法中,=>
函式宣告=> expression
是{ return expression; }
的縮寫。
使用預設的質感設計主題,按鈕的顏色為黑色,要在應用程式中顯示主要顏色的圖示,可以使用不同的主題。
圖示從IconTheme
控制元件繼承其顏色、不透明度和大小,該控制元件使用IconThemeData
物件來定義這些特徵。將IconTheme
控制元件中的_buildTextComposer()
方法中的所有控制元件包裝起來,並使用其data
屬性來指定當前主題的ThemeData
物件。這給了按鈕(和控制元件樹的這一部分中的任何其他圖示)當前主題的主要顏色。
class ChatScreenState extends State<ChatScreen> {
//...
Widget _buildTextComposer() {
return new IconTheme(
data: new IconThemeData(color: Theme.of(context).accentColor),
child: new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget> [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '傳送訊息'),
)
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text)
),
)
]
)
)
);
}
//...
}
_buildTextComposer()
方法可以從其封裝State
物件訪問BuildContext
物件,您不需要明確地將上下文傳遞給該方法。