【Flutter 1.5】製作一個簡單的名片列表APP
主要這一篇文章是要做一個比較完整的案例,個人一直覺得『挫折感才是成功的捷徑』,一直做『對的』事情很難成長的,唯有『挫折感』才能讓人記憶深刻,像我的好人卡已經打破櫻木花道50+的記錄了,真是めでたしめでたし啊,等我要推出好人撲克牌的時候,再麻煩大家再來抖內,集資一下吧。話不多說,大家一起來從做中學吧,以下只會將部分的Code貼上解說。
架構
畫面架構
- 在圖上可以看到一共有三個畫面,一個是登入的畫面,一個是清單列表,一個是介紹的內容,很符合一般APP的長相。
檔案架構
- 如圖上所示,畫面放在『libs/widgets』內,有關資料的長相就放在『libs/models』中,而『libs/helpers』則是放一些公用程式,資源檔 (圖片、影音、文件…)都放在『assets』之中。
常數
// Constants.dart
import 'package:flutter/material.dart';
// 文字
const appTitle = "MyTableViewApp";
const pinCodeHintText = "Pin Code";
const loginButtonText = "Login";
// 常數
const bigRadius = 66.0;
const buttonHeight = 24.0;
// 顏色
Color appDarkGreyColor = Color.fromRGBO(58, 66, 86, 1.0);
Color appGreyColor = Color.fromRGBO(64, 75, 96, .9);
// 圖片
Image appLogo = Image.asset('assets/images/flutter-logo-round.jpg');
// 頁面Tag
const loginPageTag = 'Login Page';
const homePageTag = 'Home Page';
主程式
void main() => runApp(MyTableViewApp());
class MyTableViewApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
loginPageTag: (context) => LoginPage(),
homePageTag: (context) => HomePage(),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
theme: ThemeData(primaryColor: appDarkGreyColor,),
home: LoginPage(),
routes: routes,
);
}
}
登入畫面
- 在登入畫面中主要的內容有三塊,一是圓角圖片框 - CircleAvatar,再來是輸入框 - TextFormField,而後是凸起按鈕 - RaisedButton
新增檔案
- 首先,我們先開啟一個LoginPage.dart檔案,當做登入頁面寫Code的地方,
Logo圖示
/// 產生logo圖示
CircleAvatar _appLogoMaker() {
var circleAvatar = CircleAvatar(
backgroundColor: Colors.transparent,
radius: bigRadius,
child: appLogo,
);
return circleAvatar;
}
取得圖片
- 不得不說flutter在取得網路圖片上很簡單,iOS或Android要取得網路上的圖片一般都需要安裝套件,或者是寫一堆Code,但是flutter內建的Image Class一行就OK了。
// 從本機內取得
Image localImage = Image.asset('assets/images/flutter-logo-round.jpg');
// 從網路上取得 (緩存)
Image networkImage = Image.network('https://cdn-images-1.medium.com/max/1600/1*TFZQzyVAHLVXI_wNreokGA.jpg');
製作圖片圓角
- 在這裡可以使用CircleAvatar來製作圓角,圖的大小就由radius屬性來決定。
CircleAvatar(radius: 25.0,);
產生輸入框
/// 產生輸入框
TextFormField _appPinCodeMaker() {
var textFormField = TextFormField(
controller: _pinCodeController,
keyboardType: TextInputType.phone,
maxLength: 4,
minLines: 1,
autofocus: true,
decoration: InputDecoration(
hintText: pinCodeHintText,
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0),),
hintStyle: TextStyle(color: Colors.white,)
),
style: TextStyle(color: Colors.white),
);
return textFormField;
}
控制器
- controller這個屬性是用來看看是誰來控制它 (取得文字)
鍵盤的樣式
- keyboardType是用來設定鍵盤的樣式
字數及行數
- maxLength / minLines是用來設定輸入的字數及最小行數
產生按鈕
- 其中比較重要的就是,按下去會有什麼反應 - RaisedButton的onPressed屬性,利用Navigator跳到下一頁去
/// 產生按鈕
RaisedButton _raisedButtonMaker(BuildContext context) {
var raisedButton = RaisedButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
padding: EdgeInsets.all(12),
color: appDarkGreyColor,
onPressed: () { Navigator.of(context).pushNamed(homePageTag); },
child: _buttonTextMaker(),
);
return raisedButton;
}
Widget合體
- 最後利用ListView - Android也是叫ListView來組合Widget,其中SizedBox就是空白的框,再把整個外框架構用Scaffold包起來輸出。
/// 整體架構
Scaffold appScaffold(BuildContext context) {
final logo = _appLogoMaker();
final pinCode = _appPinCodeMaker();
final loginButton = _appLoginButtonMaker(context);
Scaffold scaffold = Scaffold(
backgroundColor: appDarkGreyColor,
body: Center(
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.only(left: 24.0, right: 24.0),
children: <Widget>[
logo,
SizedBox(height: bigRadius,),
pinCode,
SizedBox(height: buttonHeight,),
loginButton
],
),
),
);
return scaffold;
}
人員清單
Card的組成
- 這裡就不再細部作介紹了,相信大家能看到這裡,就已經是Layout界的高高手了
/// 取得大頭照
CircleAvatar _getRecordPhoto(Record record) {
CircleAvatar circleAvatar = CircleAvatar(
radius: 32,
backgroundImage: NetworkImage(record.photo),
);
return circleAvatar;
}
/// 取得內容文字 (介紹)
RichText _getRecordRichText(Record record) {
RichText richText = RichText(
text: TextSpan(
text: record.address,
style: TextStyle(color: Colors.white),
),
maxLines: 3,
softWrap: true,
);
return richText;
}
/// 取得內容文字 (姓名)
Text _getRecordText(Record record) {
Text text = Text(
record.name,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
);
return text;
}
/// 取得箭頭圖示
Icon _getArrowIcon() {
Icon icon = Icon(Icons.keyboard_arrow_right, color: Colors.white, size: 30.0);
return icon;
}
資料的取得
- 相信大家可以看到上面的程式都有一個變數叫『Record record』,其實它就是去讀取『records.json』的資料來使用 (雖然一般都是去讀取網路上的資料),主要的處理都是在『models』資料夾之內
資料模型
- Record.dart跟RecordList.dart就是存資料的樣子,裡面比較重要的就是factory關鍵字,聽名字就知道它是一個工廠方法,利用fromJson()來將JSON字串轉成Class,這個動作就叫做反序列化
// Record.dart
class Record {
String name;
String address;
String contact;
String photo;
String url;
Record({this.name, this.address, this.contact, this.photo, this.url});
factory Record.fromJson(Map<String, dynamic> json) {
var record = Record(
name: json['name'],
address: json['address'],
contact: json['contact'],
photo: json['photo'],
url: json['url']);
return record;
}
}
// RecordList.dart
import 'Record.dart';
class RecordList {
List<Record> records = List();
RecordList({this.records});
factory RecordList.fromJson(List<dynamic> parsedJson) {
List<Record> records = List<Record>();
records = parsedJson.map((i) => Record.fromJson(i)).toList();
return RecordList(
records: records,
);
}
}
取得資料
- 利用async / await來取得網路上的資料(非同步),再使用setState()來改變畫面上的樣子(重畫),這個動作是StatefulWidget才會有的,故名思義,它就是會改變的Widget,當然也有不會改變的StatelessWidget,我想應該是為了效能才有這兩者的分別才是。
/// 取得資料
void _getRecords() async {
RecordList records = await RecordService().loadRecords();
setState(() {
for (Record record in records.records) {
this._records.records.add(record);
this._filteredRecords.records.add(record);
}
});
}
產生搜尋列
- 其實它就是在AppBar上加上一個TextField輸入文字,來進行搜尋
/// 搜尋功能
void _searchPressed() {
setState(() {
if (this._searchIcon.icon != Icons.search) { _closeSerachBar(); return; }
_openSerachBar();
});
}
/// 把SearchBar打開
void _openSerachBar() {
this._searchIcon = Icon(Icons.close);
this._appBarTitle = TextField(
controller: _filterTextEditingController,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.white),
fillColor: Colors.white,
hintText: 'Search by name',
hintStyle: TextStyle(color: Colors.white),
),
);
}
/// 把SearchBar關掉
void _closeSerachBar() {
this._searchIcon = Icon(Icons.search);
this._appBarTitle = Text(appTitle);
_filterTextEditingController.clear();
}
過濾資料
- 就是先把文字轉成小寫再去做文字的比對
/// 過濾資料 (轉小寫比較)
void _filterRecords() {
if (_searchText.isNotEmpty) {
_filteredRecords.records = List();
for (int i = 0; i < _records.records.length; i++) {
if (_records.records[i].name.toLowerCase().contains(_searchText.toLowerCase()) || _records.records[i].address.toLowerCase().contains(_searchText.toLowerCase())) {
_filteredRecords.records.add(_records.records[i]);
}
}
}
}
內文頁
- 這頁的畫面跟上頁用的widget差不多,就不再多說了,這頁最主要的功能是安裝第三方套件 - url_launcher來開啟網頁,而安裝設定的位置則是在pubspec.yaml上
# pubspec.yaml
name: flutter_uitableview
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
url_launcher: ^5.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/flutter-logo-round.jpg
- assets/data/records.json
import 'package:url_launcher/url_launcher.dart';
class URLLauncher {
/// 開啟網頁
void launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
}
結果
範例程式碼下載
後記
- 因為個人也是個初學者,Code雖然Key得出來,也能夠Run,但是對內容其實不是很瞭解,還是要多多學習才是。