Flutter:仿京東專案實戰(3)-商品詳情頁功能實現
highlight: a11y-dark
在我個人認為學習一門新的語言(快速高效學習) 一定是通過實踐,最好的就是做專案,這裡我會簡單寫一個京東的Demo。
第一天 搭建專案框架,實現首頁的功能:https://juejin.cn/editor/drafts/7043351582573854727。
第二天實現 分類和商品列表頁面: https://juejin.cn/post/7044716539550892068。
Flutter-混合工程的持續整合實踐: https://juejin.cn/post/7042099001679675422
Dart2.15版本釋出了:https://mp.weixin.qq.com/s/g-1uCl3upI-JYHxUeEIbKg
前面兩篇文章分別完成了首頁和分類及商品列表頁面功能,這篇文章完成商品詳情頁的功能,這裡用到了以下知識點:
用到的知識點
1. Provider 狀態管理
什麼是Provider 狀態管理?
當我們想在多個頁面(元件/Widget)之間共享狀態(資料),或者一個頁面(組 件/Widget)中的多個子元件之間共享狀態(資料),這個時候我們就可以用 Flutter 中的狀態管理來管理統一的狀態(資料),實現不同元件直接的傳值和資料共享。provider
是 Flutter 官方團隊
推出的狀態管理模式。
具體的使用:
- 配置
provider: ^6.0.1
, - 新建一個資料夾叫 provider,在 provider 資料夾裡面放我們對於的狀態管理類
- 在 provider 裡面新建 counter.dart
- counter.dart 裡面新建一個類繼承
minxins
的ChangeNotifier
程式碼如下
```dart import 'package:provider/provider.dart'; class Counter with ChangeNotifier { int _count; Counter(this._count);
void add() { _count++; notifyListeners();//2 } get count => _count;//3 } ```
notifyListeners();
這個方法是通知用到Counter
物件的widget重新整理用的
- 找到 main.dart 修改程式碼,新增
MultiProvider
dart
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
//配置設計稿的寬度高度
designSize: Size(750, 1334),
builder:()=> MultiProvider(
providers:[
ChangeNotifierProvider(create: (_) => Counter()),
],
child: MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate, // 指定本地化的字串和一些其他的值
GlobalCupertinoLocalizations.delegate, // 對應的Cupertino風格
GlobalWidgetsLocalizations.delegate //指定預設的文字排列方向, 由左到右或由右到左
],
supportedLocales: [
Locale("en"),
Locale("zh")
],
initialRoute: '/',
onGenerateRoute: onGenerateRoute)));
}
}
- 獲取值、以及設定值
```dart import 'package:provider/provider.dart'; import '../../provider/Counter.dart';
Widget build(BuildContext context) {
final counter = Provider.of
用Provider.of
用Provider.of
2. eventBus 廣播
-
配置
event_bus: ^2.0.0
-
新建 event_bus.dart 類統一管理
```dart //引入 eventBus 包檔案 import 'package:event_bus/event_bus.dart';
//建立EventBus EventBus eventBus = new EventBus();
//event 監聽 class EventFn{ //想要接收的資料時什麼型別的,就定義相同型別的變數 dynamic obj; EventFn(this.obj); } ``` - 在需要廣播事件的頁面引入上面的 EventBus.dart 類 然後配置如下程式碼
dart
eventBus.fire(new EventFn('資料'));
- 在需要監聽廣播的地方引入上面的
event_bus.dart
類 然後配置如下程式碼
dart
void initState() {
super.initState();
//監聽廣播
eventBus.on<EventFn>().listen((event){
print(event);
});
}
- event_bus 取消事件監聽
dart @override void dispose() { super.dispose(); //取消訂閱 eventBusFn.cancel(); }
3. flutter_inappwebview 載入網頁
- 配置
flutter_inappwebview: ^5.3.2
- 引入包檔案
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
- 初始化屬性
initialUrl: 被載入的初始URL。 initialOptions:將被載入的初始URL; initialOptions:將被使用的初始WebView選項。將要使用的初始WebView選項。 gestureRecognizers:指定哪些手勢應該被WebView消耗。 initialData:初始InAppWebViewInitial資料。將要載入的InAppWebViewInitialData的初始資料,比如一個HTML字串。 initialFile:將被載入的初始資產檔案。 initialHeaders: 將要使用的初始標頭檔案。將要使用的初始標頭檔案。 contextMenu:上下文選單,包含自定義選單項。上下文選單,包含自定義選單項。
-
常用觸發的事件
onLoadStart:當WebView開始載入一個URL時被觸發的事件。 onLoadStop:當WebView完成載入一個URL時觸發的事件。 onLoadHttpError:當WebView主頁面收到一個HTTP錯誤時被觸發的事件。 onConsoleMessage:當WebView收到JavaScript控制檯訊息(如console.log ,console.error等)時觸發的事件。 shouldOverrideUrlLoading:噹噹前WebView中的URL即將被載入時,給主機應用程式一個控制的機會。 onDownloadStart:當WebView識別到一個可下載的檔案時發射的事件。 onReceivedHttpAuthRequest:當WebView接收到HTTP認證請求時觸發的事件。預設行為是取消該請求。 onReceivedServerTrustAuthRequest:當WebView需要執行伺服器信任認證(證書驗證)時被觸發的事件。 onPrint:當window.print()從JavaScript端被呼叫時被觸發的事件,預設行為是取消請求;onCreateWindow:當WebView需要進行伺服器信任驗證(證書驗證)時被觸發的事件。 onCreateWindow: 當InAppWebView請求主機應用程式建立一個新視窗時,例如當試圖開啟一個target="_blank"的連結或當window.open()被JavaScript端呼叫時,事件被觸發。
-
簡單使用
dart
Expanded(
child: InAppWebView(
initialUrlRequest: URLRequest(url: Uri.parse("https://jdmall.itying.com/pcontent?id=${_id}")),
onProgressChanged: (InAppWebViewController controller, int progress){
if (progress / 100 > 0.9999) {
setState(() {
this._flag = false;
});
}
},
)
)
更多更詳細用法的可以參考這篇文章:https://juejin.cn/post/6869291513508659213
4. DefaultTabController和TabController
這兩個都可以實現頂部導航選項卡,區別就是TabController一般放在有狀態元件中使用,而DefaultTabController一般放在無狀態元件中使用,這裡沒有做成上下拉重新整理,在這個頁面用的是DefaultTabController。
TabController介紹
- 常見的屬性
- 常用方法介紹
TabBar屬性介紹
dart
const TabBar({
Key key,
@required this.tabs,//必須實現的,設定需要展示的tabs,最少需要兩個
this.controller,
this.isScrollable = false,//是否需要滾動,true為需要
this.indicatorColor,//選中下劃線的顏色
this.indicatorWeight = 2.0,//選中下劃線的高度,值越大高度越高,預設為2
this.indicatorPadding = EdgeInsets.zero,
this.indicator,//用於設定選中狀態下的展示樣式
this.indicatorSize,//選中下劃線的長度,label時跟文字內容長度一樣,tab時跟一個Tab的長度一樣
this.labelColor,//設定選中時的字型顏色,tabs裡面的字型樣式優先順序最高
this.labelStyle,//設定選中時的字型樣式,tabs裡面的字型樣式優先順序最高
this.labelPadding,
this.unselectedLabelColor,//設定未選中時的字型顏色,tabs裡面的字型樣式優先順序最高
this.unselectedLabelStyle,//設定未選中時的字型樣式,tabs裡面的字型樣式優先順序最高
this.dragStartBehavior = DragStartBehavior.start,
this.onTap,//點選事件
})
DefaultTabController的使用
dart
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: ScreenAdapter.width(400),
child: TabBar(
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
labelColor: Colors.red,
unselectedLabelColor: Colors.white,
tabs: [
Tab(
child: Text('商品', style: TextStyle(fontSize: 18,),),
),
Tab(
child: Text('詳情', style: TextStyle(fontSize: 18),),),
Tab(
child: Text('評價', style: TextStyle(fontSize: 18),),
)
],
),
)
],
),
),
body:Stack(
children: [
TabBarView(children:
[
ProductContentFirst(_productContentList),
ProductContentSecond(_productContentList),
ProductContentThrid(),
]),
],
),
))
showModalBottomSheet 底部面板
ModalBottomSheet
底部面板,相當於彈出了一個新頁面,有點類似於 ActionSheet
ModalBottomSheet的屬性:
- context:BuildContext
- builder:WidgetBuilder
- backgroundColor:背景色
- elevation:陰影
- shape:形狀
- barrierColor:遮蓋背景顏色
- isDismissible:點選遮蓋背景是否可消失
- enableDrag:下滑消失
BoxDecoration 的使用說明
BoxDecoration通常用於給Widget元件設定邊框效果、陰影效果、漸變色等效果;常用屬性如下:
實現效果
具體實現程式碼
建立product_content_model.dart
類
```dart class ProductContentModel { late ProductContentitem result;
ProductContentModel({ required this.result, });
ProductContentModel.fromJson(Map
class ProductContentitem {
//可為空的欄位就設定成可為空空
String? sId;
String? title;
String? cid;
Object? price;
Object? oldPrice;
Object? isBest;
Object? isHot;
Object? isNew;
late List
ProductContentitem({ this.sId, this.title, this.cid, this.price, this.oldPrice, this.isBest, this.isHot, this.isNew, required this.attr, this.status, required this.pic, this.content, this.cname, this.salecount, this.subTitle, });
ProductContentitem.fromJson(Map
class Attr {
late String cate;
late List
Attr({ required this.cate, required this.list, });
Attr.fromJson(Map
Map
商品詳情頁的框架頁面
```dart class ProductContentPage extends StatefulWidget {
final Map arguments; ProductContentPage({Key? key, required this.arguments}) : super(key: key);
@override _ProductContentPageState createState() => _ProductContentPageState(); }
class _ProductContentPageState extends State
List _productContentList=[];
@override void initState() { // TODO: implement initState super.initState();
_getContentData();
}
//請求商品資料 _getContentData() async{
var api ='${Config.domain}api/pcontent?id=${widget.arguments['id']}';
print(api);
var result = await Dio().get(api);
var productContent = new ProductContentModel.fromJson(result.data);
setState(() {
_productContentList.add(productContent.result);
});
}
@override Widget build(BuildContext context) { //實現頂部導航選項卡 return DefaultTabController(length: 3, child: Scaffold( appBar: AppBar( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: ScreenAdapter.width(400), child: TabBar( indicatorColor: Colors.red, indicatorSize: TabBarIndicatorSize.label, labelColor: Colors.red, unselectedLabelColor: Colors.white, tabs: [ Tab( child: Text('商品', style: TextStyle(fontSize: 18,),), ), Tab( child: Text('詳情', style: TextStyle(fontSize: 18),), ), Tab( child: Text('評價', style: TextStyle(fontSize: 18),), ) ], ), ) ], ), actions: [ IconButton(onPressed: (){ //實現選單選項欄 showMenu(context: context, position: RelativeRect.fromLTRB(ScreenAdapter.width(600), ScreenUtil().statusBarHeight+40, 10, 0), items: [ PopupMenuItem( child: Row( children: [ Icon(Icons.home), SizedBox(width: 10,), Text('首頁') ], ), ), PopupMenuItem( child: Row( children: [ Icon(Icons.search), SizedBox(width: 10,), Text('搜尋') ], ), ) ] ); }, icon: Icon(Icons.more_horiz)), ], ), body: _productContentList.length > 0 ? Stack( children: [ TabBarView(children: [ //商品頁面 ProductContentFirst(_productContentList), //詳情頁面 ProductContentSecond(_productContentList), //評價頁面 ProductContentThrid(), ] ), //頁面底部的購物車、加入購物車、立即購買 Positioned( width: ScreenAdapter.width(750), height: ScreenAdapter.width(100)+ScreenAdapter.bottomBarHeight+10, bottom: 0, child: Container( decoration: BoxDecoration( border: Border( top: BorderSide( width: 1, color: Colors.black26 ) ), color: Colors.white ), child: Container( margin: EdgeInsets.only(top: 10, bottom: ScreenAdapter.bottomBarHeight), child: Row( children: [ Container( width: 100, height: ScreenAdapter.height(100), child: Column( children: [ Icon(Icons.shopping_cart, size: ScreenAdapter.width(38),), Text('購物車', style: TextStyle(fontSize:ScreenAdapter.size(24))), ], ), ), Expanded( flex: 1, child: CircleButton( color: Color.fromRGBO(253, 1, 0, 0.9), text: '加入購物車', callBack: (){ print('加入購物車'); }, ) ), Expanded( flex: 1, child: CircleButton( color: Color.fromRGBO(255, 165, 0, 0.9), text: '立即購買', callBack: (){ print('立即購買'); }, ) ) ], ), ), ), ), ], ) : LoadingWidget(), )); } } ```
商品詳情頁的商品頁面
```dart class ProductContentFirst extends StatefulWidget { final List _productContentList; ProductContentFirst(this._productContentList, {Key? key}) : super(key: key);
@override _ProductContentFirstState createState() => _ProductContentFirstState(); }
class _ProductContentFirstState extends State
late ProductContentitem _productContent;
List _attr = [];
String _selectedValue='';
var cartProvider;
@override void initState() { // TODO: implement setState super.initState();
_productContent = widget._productContentList[0];
_attr = _productContent.attr;
_selectedValue = _attr.first.list.first;
}
//實現選項卡功能
_attrBottomSheet(){
showModalBottomSheet(
context: context,
builder: (context){
return Stack(
children: [
Container(
padding: EdgeInsets.only(left: 10),
child: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _getAttrWidget(),
),
Divider(),
Container(
margin: EdgeInsets.only(top: 10),
height: ScreenAdapter.height(80),
child: Row(
children:
SizedBox(width: 10),
CartNum(this._productContent)
],
),
)
],
),
),
Positioned(
bottom: 0,
width: ScreenAdapter.width(750),
height: ScreenAdapter.height(76)+ScreenAdapter.bottomBarHeight,
child: Container(
color: Colors.white,
padding: EdgeInsets.only(bottom: ScreenAdapter.bottomBarHeight),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
child: CircleButton(
color: Color.fromRGBO(253, 1, 0, 0.9),
text: "加入購物車",
callBack: () async {
print('豪傑是八點就把手');
await CartServices.addCart(this._productContent);
//關閉底部篩選屬性
Navigator.of(context).pop();
//呼叫Provider 更新資料
this.cartProvider.updateCartList();
Fluttertoast.showToast( msg: '加入購物車成功', toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER,);
},
),
),
),
Expanded(
flex: 1,
child: Container(
margin: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: CircleButton(
color: Color.fromRGBO(255, 165, 0, 0.9),
text: "立即購買",
callBack: () {
print('立即購買');
},
)),
)
],
),
),
)
],
);
}
);
}
List
return attrList;
}
List
@override
Widget build(BuildContext context) {
this.cartProvider = Provider.of
//處理圖片
String pic = Config.domain + this._productContent.pic;
pic = pic.replaceAll('\', '/');
//商品頁面內容
return Container(
padding: EdgeInsets.all(10),
child: ListView(
children: [
AspectRatio(
aspectRatio: 16/9,
child: Image.network(pic, fit: BoxFit.cover,),
),
Container(
padding: EdgeInsets.only(top: 10),
child: Text(_productContent.title!,
style: TextStyle(color: Colors.black87, fontSize: ScreenAdapter.size(36), fontWeight: FontWeight.bold),),
),
Container(
padding: EdgeInsets.only(top: 10),
child: Text(
_productContent.subTitle!,
style: TextStyle(
color: Colors.black54,
fontSize: ScreenAdapter.size(28))
)
),
SizedBox(height: 10,),
Container(
child: Row(
children: [
Expanded(
child: Row(
children: [
Text('特價:'),
Text('¥${_productContent.price}',style: TextStyle(
color: Colors.red,
fontSize: ScreenAdapter.size(46))),
],
)
),
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('原價:'),
Text('¥${_productContent.oldPrice}',style: TextStyle(
color: Colors.black38,
fontSize: ScreenAdapter.size(28),
decoration: TextDecoration.lineThrough)),
],
),
)
],
),
),
//篩選
_attr.length > 0
? Container(
margin: EdgeInsets.only(top: 10),
height: ScreenAdapter.height(80),
child: InkWell(
onTap: () {
_attrBottomSheet();
},
child: Row(
children: <Widget>[
Text("已選: ",
style: TextStyle(fontWeight: FontWeight.bold)),
Text("${_selectedValue}")
],
),
),
)
: Text(""),
Divider(),
Container(
height: ScreenAdapter.height(80),
child: Row(
children: <Widget>[
Text("運費: ", style: TextStyle(fontWeight: FontWeight.bold)),
Text("免運費")
],
),
),
Divider(),
],
),
);
} } ```
這個就是程式碼中說的選項卡,也是通過介面返回資料生成的
商品詳情頁面
```dart class ProductContentSecond extends StatefulWidget {
final List _productContentList;
const ProductContentSecond(this._productContentList, {Key? key}) : super(key: key);
@override _ProductContentSecondState createState() => _ProductContentSecondState(); }
class _ProductContentSecondState extends State
var _flag=true;
var _id;
@override void initState() { // TODO: implement initState super.initState();
_id = widget._productContentList[0].sId;
}
@override Widget build(BuildContext context) { return Container( child: Column( children: [ _flag ? LoadingWidget() : Text(''), Expanded( child: InAppWebView( initialUrlRequest: URLRequest(url: Uri.parse("https://jdmall.itying.com/pcontent?id=${_id}")), onProgressChanged: (InAppWebViewController controller, int progress){ if (progress / 100 > 0.9999) { setState(() { this._flag = false; }); } }, ) ) ], ), ); } } ```
後面我會把整個專案的程式碼放到github.
- Flutter:仿京東專案實戰(4)-購物車頁面功能實現
- Flutter整合原生遇到的問題彙總
- Flutter:仿京東專案實戰(3)-商品詳情頁功能實現
- Flutter-Dart中的非同步和多執行緒講解
- iOS-底層原理分析之Block本質
- Flutter-官方推薦的Flutter與原生互動外掛Pigeon
- Flutter-flutter_sound錄音與播放
- iOS-CocoaPods的原理及Podfile.lock問題
- iOS配置多環境的三種方案
- iOS-各種Crash防護
- iOS-Swift中常見的幾種閉包
- Flutter:仿京東專案實戰(2)-分類和商品列表頁面功能實現
- Flutter:仿京東專案實戰(1)-首頁功能實現
- Flutter-JSON轉Model的四種便捷方案
- Flutter-導航與路由堆疊詳解
- Flutter 與原生通訊的三種方式
- iOS-記憶體洩漏檢測
- Fastlane實現自動打包
- 懶人必備神器-Xcode程式碼塊
- Jenkins實現自動化打包