經過這一段對 Flutter 的瞭解和接觸,掌握如何完整使用 Flutter 開發一個項目。實際上,在 Flutter 中,一切皆 widget,我們的界面都是由各種 widget 堆疊出來的。
一個 Flutter 工程涉及以下幾個要點:
- 工程項目代碼分層
- 主題風格
- 插件
- 路由
- 網絡數據交互
- 界面佈局及刷新
一、工程項目代碼分層
一個正式的工程項目,它的代碼必須做到分層,代碼的分層體現了開始者的架構能力。
Flutter 工程的主要工作 lib 目錄及 pubspec.yaml :
- main.dart:Flutter 的入口函數
- loading.dart:啓動頁,一般生存週期爲3-5秒
- app.dart:工程主文件
- conf : 配置文件目前或一些宏定義數據文件目錄
- model : 數據模型目錄
- pages : 各 UI ,即 Widget 文件
- service : 網絡請求目錄
- style : 自定義風格文件(顏色、字體等)
- utils : 工具目錄
代碼分層設計設計的合不合理,直接影響代碼的可維護性和穩定性。
二、主題風格
Flutter 默認的主題是 藍白 的風格,其他主題則需要配置。依項目而定,根據當前需要,配置一個 紅灰 風格:
#main.dart
void main() => runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter實戰',
//自定義主題
theme: mDefaultTheme,
));
//自定義主題
final ThemeData mDefaultTheme = ThemeData(
primaryColor: Colors.redAccent,
);
Colors.redAccent 爲系統主題,在 colors.dart 中定義:
static const MaterialAccentColor redAccent = MaterialAccentColor(
_redAccentValue,
<int, Color>{
100: Color(0xFFFF8A80),
200: Color(_redAccentValue),
400: Color(0xFFFF1744),
700: Color(0xFFD50000),
},
);
static const int _redAccentValue = 0xFFFF5252;
當然也可以自定義一些風格或顏色,在工程 style 中 color.dart:
//產品顏色
class ProductColors{
static const Color bgColor = Color(0xFFFFFFFF);
static const divideLineColor = Color.fromRGBO(245, 245, 245, 1.0);
static const typeColor = Color.fromRGBO(182, 9, 9, 1.0);
static const piontColor = Color.fromRGBO(132, 95, 63, 1.0);
}
三、插件
開發者不可能對每個功能都自已造輪子,選取合適的輪子,對於項目來說,可以達到事半功倍的效果。
3.1 添加第三方庫
打開 pubspec.yaml 文件添加三方庫,可以 在 https://pub.dev/flutter (需要FQ)上找到許多開源軟件包
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
http: ^0.12.0
flutter_webview_plugin: ^0.3.0
flutter_swiper: 1.1.4
- http : 網絡請求插件
- flutter_webview_plugin : 一些靜態頁面的加載,使用webview
- flutter_swiper : 動畫輪播效果
3.2 導入
swiper 插件
點擊 Packages get 獲取剛添加的包。
這樣就可以使用這個庫了。
四、路由
路由主要用於界面的切換及跳轉,分爲 靜態路由 和 動態路由
- 靜態路由:界面的跳轉不帶附加參數
- 動態路由:界面的跳轉可攜帶附加參數
4.1 路由初始化
void main() => runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter實戰',
//自定義主題
theme: mDefaultTheme,
//添加路由
routes: <String,WidgetBuilder>{
"app": (BuildContext context) => App(),
"company_info":(BuildContext context) => WebviewScaffold(
url: "https://www.baidu.com",
appBar: AppBar(
title: Text('公司介紹'),
leading: IconButton(
icon: Icon(Icons.home),
onPressed: (){
//路由到主界面
Navigator.of(context).pushReplacementNamed('app');
},
),
),
),
},
//指定加載頁面
home: LoadingPage(),
));
4.2 靜態路由
Navigator.of(context).pushReplacementNamed('company_info');
4.3 動態路由
Navigator.push(context,MaterialPageRoute(builder: (context) => AboutContactPage()));
或者
Navigator.push(context,MaterialPageRoute(builder: (context) => NewsDetailPage(item: item)),
數據接收處理:
class NewsDetailPage extends StatelessWidget{
final NewsItemModal item;
NewsDetailPage({Key key,@required this.item}) : super(key:key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(item.title),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text(item.content),
),
);
}
}
五、網絡數據交互
網絡數據交互一般涉及 URL、數據模型、數據交互模式(Get、Post)等
5.1 URL 定義
在配置中定義宏變量
class Config{
static const String IP = '192.168.2.5';
static const String PORT = '8080';
}
5.2 數據模型
//新聞列表項數據轉換
class NewsItemModal{
String author;//作者
String title;//標題
String content;//內容
NewsItemModal({
this.author,
this.title,
this.content,
});
factory NewsItemModal.fromJson(dynamic json){
return NewsItemModal(
author: json['author'],
title: json['title'],
content: json['content'],
);
}
}
//新聞列表數據轉換
class NewsListModal{
List<NewsItemModal> data;
NewsListModal(this.data);
factory NewsListModal.fromJson(List json){
return NewsListModal(
json.map((i) => NewsItemModal.fromJson((i))).toList()
);
}
}
5.3 數據交互模式
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../conf/configure.dart';
//獲取新聞數據
getNewsResult() async {
String url = 'http://' + Config.IP + ':' + Config.PORT + '/?action=getNews';
var res = await http.get(url);
String body = res.body;
var json= jsonDecode(body);
print(json);
return json['items'] as List;
}
六、界面佈局及刷新
6.1 啓動頁加載
一個存在 3 – 5 秒的界面
class LoadingPage extends StatefulWidget{
@override
_LoadingState createState() => _LoadingState();
}
class _LoadingState extends State<LoadingPage>{
@override
void initState(){
super.initState();
//在加載頁面停頓3秒
Future.delayed(Duration(seconds: 3),(){
print('Flutter企業站啓動...');
Navigator.of(context).pushReplacementNamed("app");
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Center(
child: Stack(
children: <Widget>[
//加載頁面背景圖
Image.asset(
'assets/images/loading.jpeg'
),
Center(
child: Text(
'Flutter',
style: TextStyle(
color: Colors.white,
fontSize: 36.0,
decoration: TextDecoration.none
),
),
),
],
),
),
);
}
}
6.2 輪播圖片
輪播圖片應用很廣泛,如廣告宣傳之類。
在資源配置圖片中,添加圖片
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/loading.jpeg
- assets/images/company.jpg
#輪播圖片
- assets/images/banners/1.jpeg
- assets/images/banners/2.jpeg
- assets/images/banners/3.jpeg
- assets/images/banners/4.jpeg
使用 Swiper 插件
import 'package:flutter/material.dart';
import 'package:flutter_swiper/flutter_swiper.dart';
class BannerWidget extends StatelessWidget{
//圖片路徑
List<String> banners = <String>[
'assets/images/banners/1.jpeg',
'assets/images/banners/2.jpeg',
'assets/images/banners/3.jpeg',
'assets/images/banners/4.jpeg',
];
@override
Widget build(BuildContext context) {
//計算寬高 按比例
double width = MediaQuery.of(context).size.width;
double height = width * 540.0 / 1080.0;
return Container(
width: width,
height: height,
//輪播組件
child: Swiper(
itemBuilder: (BuildContext context, index){
return Container(
//圖片左右內邊距
margin: EdgeInsets.only(left: 5, right: 5),
child: Image.asset(
banners[index],
width: width,
height: height,
fit: BoxFit.cover,
),
);
},
//輪播數量
itemCount: banners.length,
//方向
scrollDirection: Axis.horizontal,
//是否自動播放
autoplay: true,
),
);
}
}
6.3 主界面(含導航欄)
import 'package:flutter/material.dart';
import 'pages/about_us_page.dart';
import 'pages/home_page.dart';
import 'pages/news_page.dart';
import 'pages/product_page.dart';
class App extends StatefulWidget {
@override
AppState createState() => AppState();
}
class AppState extends State<App> {
//當前選擇頁面索引
var _currentIndex = 0;
HomePage homePage;
ProductPage productPage;
NewsPage newsPage;
AboutUsPage aboutUsPage;
//根據當前索引返回不同的頁面
currentPage(){
switch(_currentIndex){
case 0:
if(homePage == null){
homePage = HomePage();
}
return homePage;
case 1:
if(productPage == null){
productPage = ProductPage();
}
return productPage;
case 2:
if(newsPage == null){
newsPage = NewsPage();
}
return newsPage;
case 3:
if(aboutUsPage == null){
aboutUsPage = AboutUsPage();
}
return aboutUsPage;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter企業站實戰'),
leading: Icon(Icons.home),
actions: <Widget>[
//右側內邊距
Padding(
padding: EdgeInsets.only(right: 20.0),
child: GestureDetector(
onTap: () {},
child: Icon(
Icons.search,
),
),
),
],
),
body: currentPage(),
//底部導航欄
bottomNavigationBar: BottomNavigationBar(
//通過fixedColor設置選中item 的顏色
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
onTap: ((index) {
setState(() {
_currentIndex = index;
});
}),
//底部導航欄
items: [
BottomNavigationBarItem(
title: Text(
'首頁',
),
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
title: Text(
'產品',
),
icon: Icon(Icons.apps),
),
BottomNavigationBarItem(
title: Text(
'新聞',
),
icon: Icon(Icons.fiber_new),
),
BottomNavigationBarItem(
title: Text(
'關於我們',
),
icon: Icon(Icons.insert_comment),
),
]),
);
}
}
6.4 ListView 的應用
import 'package:flutter/material.dart';
import '../model/news.dart';
import '../services/news.dart';
import 'news_detail_page.dart';
//新聞頁面
class NewsPage extends StatefulWidget {
@override
NewsPageState createState() => NewsPageState();
}
class NewsPageState extends State<NewsPage> {
NewsListModal listData = NewsListModal([]);
//獲取新聞列表數據
void getNewsList() async {
var data = await getNewsResult();
NewsListModal list = NewsListModal.fromJson(data);
setState(() {
listData.data.addAll(list.data);
});
}
@override
void initState() {
getNewsList();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
//帶分隔線的List
body: ListView.separated(
//排列方向 垂直和水平
scrollDirection: Axis.vertical,
//分隔線構建器
separatorBuilder: (BuildContext contex, int index) => Divider(
height: 1.0,
color: Colors.grey,
),
itemCount: listData.data.length,
//列表項構建器
itemBuilder: (BuildContext contex, int index) {
//新聞列表項數據
NewsItemModal item = listData.data[index];
return ListTile(
title: Text(item.title),
subtitle: Text(item.content),
leading: Icon(Icons.fiber_new),
trailing: Icon(Icons.arrow_forward),
contentPadding: EdgeInsets.all(10.0),
enabled: true,
//跳轉至新聞詳情頁面
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewsDetailPage(item: item)),
);
},
);
},
),
);
}
}
6.5 文本框操作
含文本框配置及數據操作
import 'package:flutter/material.dart';
import '../services/contact.dart';
class AboutContactPage extends StatefulWidget{
@override
AboutContactPageState createState() => AboutContactPageState();
}
class AboutContactPageState extends State<AboutContactPage>{
//文本編輯控制器
TextEditingController controller = TextEditingController();
//提交數據
void commit(){
if(controller.text.length == 0){
showDialog(context: context,builder: (context) => AlertDialog(title: Text('請輸入內容'),),);
} else{
var info = contactCompany(controller.text);
print(info);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('給我留言'),
),
body: Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Image.asset(
'assets/images/company.jpg',
fit: BoxFit.cover,
),
SizedBox(
height: 40.0,
),
SizedBox(
width: 380.0,
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: '請留言',
labelText: '請留言',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
),
SizedBox(
height: 40.0,
),
SizedBox(
width: 220.0,
height: 48.0,
child: RaisedButton(
child: Text('給我們留言',style: TextStyle(fontSize: 16.0),),
color: Theme.of(context).primaryColor,//Colors.redAccent,
colorBrightness: Brightness.dark,
textColor: Colors.white,
padding: EdgeInsets.only(
left: 20.0,
right: 20.0,
top: 5.0,
bottom: 5.0,
),
shape: RoundedRectangleBorder(
side: BorderSide(
width: 1.0,
color: Colors.white,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.only(
topRight: Radius.circular(4.0),
topLeft: Radius.circular(4.0),
bottomLeft: Radius.circular(4.0),
bottomRight: Radius.circular(4.0),
),
),
onPressed: (){
commit();
},
),
),
],
),
),
);
}
}
七、注意點
- => 是 Dart 中單行函數的簡寫
- StatelessWidget 代表只有一種狀態的組件,與之對應的是 StatefulWidget(表示可能有多種狀態)。
- 在 Widget 組件中都是通過 build 方法來描述自己的內部結構。這裏的 build 表示構建 MyApp 中使用的是 MaterialApp 的系統組件。
- home標籤的值:Scaffold 是 Material library 中提供的一個組件,我們可以在裏面設置導航欄、標題和包含主屏幕 widget 樹的 body 屬性。可以看到這裏是在頁面上添加了AppBar 和一個 Text。
- Center 是一個可以把子組件放在中心的組件