花了3天时间完成了首页和个人中心主页面,目前完成情况:
首页:
个人中心:
实际效果可以去WXYX_HC下载demo,建议使用真机flutter run --release模式跑,模拟器跑debug会很卡,可能我的列表写法不够优化,有大手子可以提下改进意见。
个人中心没啥好说的,主要就是listView从上到下包各种widget,很简单。
主要说说首页的布局。
开始前先说下Json的解析,我使用的是一个工具网址Json to Dart,可以把抓包过来的Json数据转换dart的model类,使用比较简单,你也可以使用一些Json解析的第三方package。
接下来是图片缓存,Image.network()可以从网络加载图片,但并不会缓存到设备中,实际开发中图片还是要有硬盘缓存,确保不会每次启动app都重新请求,我使用了cached_network_image来缓存图片。
首页provide状态管理:
首页的所有数据操作我都放在了provide里,每当有修改操作时,添加方法并调用notifyListeners(),外界的provide<HomeProvide>会接收通知并重新build。
class HomeProvide with ChangeNotifier {
int searchCount;//搜索框数量
HomeModel homeModel;//首页主要model
HomeGroupModel groupModel;//拼团model
HomeRecGoodsModel recGoodsModel;//推荐商品model
Color searchBarBackColor = Color(0xFFAA3A1A);//搜索条默认背景色
int lastItemId = 0; //推荐最后一个商品id,分页需要
//获取首页数据
getHomeData() async {
lastItemId = 0;
searchCount = 0;
try {
await Future.wait([
//类别,banner,限时购买
networkGet(api_home).then((val) {
homeModel = HomeModel.fromJson(val);
}),
//搜索框数量
networkGet(api_search_count).then((val) {
searchCount = val['count'];
}),
//团购商品
networkGetHostChange(api_group, paramas: {'orderType': 2, 'size': 16})
.then((val) {
groupModel = HomeGroupModel.fromJson(val);
}),
//推荐商品
networkGet(api_rec, paramas: {'lastItemId': lastItemId, 'size': 20})
.then((val) {
recGoodsModel = HomeRecGoodsModel.fromJson(val);
lastItemId = recGoodsModel.rcmdItemList.last.id;
})
]).then((val) {
notifyListeners();
});
} catch (e) {
print(e);
}
}
//改变搜索栏背景色
changeSearchBarBackColor(color) {
searchBarBackColor = color;
notifyListeners();
}
//上拉加载商品推荐
Future getRecommendGoods() async {
var paramas = {'lastItemId': lastItemId, 'size': 20};
try {
await networkGet(api_rec, paramas: paramas).then((val) {
recGoodsModel.rcmdItemList.addAll(HomeRecGoodsModel.fromJson(val).rcmdItemList);
recGoodsModel.hasMore = HomeRecGoodsModel.fromJson(val).hasMore;
lastItemId = recGoodsModel.rcmdItemList.last.id;
notifyListeners();
});
} catch (e) {
print(e);
}
}
}
首页可以分为顶部搜索、banner、类目、每日拼团、限时抢购、推荐。
1. 顶部搜索+banner
合在一起说,因为它有一个效果,搜索条背景色会根据banner的切换而改变。
- flutter_swiper的使用
class HomeBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
HomeModel homeModel = Provide.value<HomeProvide>(context).homeModel;
return Container(
height: ScreenUtil().setHeight(350),
child: Swiper(
duration: 300,
pagination: SwiperPagination(),
itemCount: homeModel.focus.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
//跳转webview
},
child: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: '${homeModel.focus[index].img}',
placeholder: (context, url) {
return Image.asset(
'images/default_goods.jpeg',
);
},
errorWidget: (context, url, obj) {
return Image.asset(
'images/default_goods.jpeg',
);
}),
);
},
autoplay: true,
onIndexChanged: (index) {
//监听banner滑动,获取后台给的bannner对应颜色
String color = homeModel.focus[index].backgroundColor;
String tempColor = '0xFF' + color.substring(1);
Provide.value<HomeProvide>(context)
.changeSearchBarBackColor(Color(int.parse(tempColor)));
},
),
);
}
}
- 顶部搜索条。AnimatedContainer是一个可以实现隐式动画的容器,这里会对背景色的切换执行渐变动画。
class HomeSearchBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color backColor = Provide.value<HomeProvide>(context).searchBarBackColor;
return Provide<HomeProvide>(
builder: (context, child, val) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
color: backColor,
padding: EdgeInsets.fromLTRB(12, 12+ScreenUtil.statusBarHeight, 12, 8),
child: Column(
children: <Widget>[_title(), _searchBar(context)],
),
);
},
);
}
2. 类目
由GridView和底部滑动指示器组成,GridView的滑动方向是horizontal,副轴crossAxisCount是2,这里生成的排列效果跟真正的小程序有区别,因为这时候主轴其实是横向的,所以他item生成的顺序其实是从上到下,逐渐往右扩展。所以需要对拿到的列表数据处理一下。
滑动指示器需要监听GridView的滑动,需要使用ScrollController,给他添加监听方法。
_scrollController.addListener(() {
//监听滑动,传递滑动进度,刷新进度条
setState(() {
_process = _scrollController.position.pixels /
_scrollController.position.maxScrollExtent;
});
});
3. 每日拼团、限时抢购
主要是水平listView的构建,这个没啥好说的。
Container(
height: ScreenUtil().setWidth(230),
padding: EdgeInsets.only(left: 15, right: 15),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemExtent: ScreenUtil().setWidth(168),
itemCount: homeModel.timePurchaseItems.itemList.length,
itemBuilder: (BuildContext context, int index) {
return _item(homeModel.timePurchaseItems.itemList[index]);
},
),
);
4. 商品推荐
这里使用了flutter_staggered_grid_view不规则瀑布流
Widget _recommendView(context) {
HomeRecGoodsModel homeRecGoodsModel =
Provide.value<HomeProvide>(context).recGoodsModel;
return Container(
padding: EdgeInsets.fromLTRB(12, 10, 10, 10),
child: StaggeredGridView.countBuilder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 4,
itemCount: homeRecGoodsModel.rcmdItemList.length,
itemBuilder: (BuildContext context, int index) => new Container(
color: Colors.white,
child: _item(homeRecGoodsModel.rcmdItemList[index].categoryItem ??
homeRecGoodsModel.rcmdItemList[index].pinItem),
),
staggeredTileBuilder: (int index) => new StaggeredTile.fit(2),
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
);
}
5. 上拉加载
使用了flutter_easyrefresh,下面是主页入口的代码
注意点:
- 要在切换tab后保持滑动视图的滑动状态,需要混入mixins AutomaticKeepAliveClientMixin,并重写 wantKeepAlive 。
- 要在主页初始化ScreenUtil的设计图尺寸。
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin {
EasyRefreshController _controller;
@override
bool get wantKeepAlive => true;
@override
void initState() {
_controller = EasyRefreshController();
super.initState();
}
@override
Widget build(BuildContext context) {
//宽高自适应初始化
ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);
return Scaffold(
body: FutureBuilder(
future: _getHomeData(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Provide<HomeProvide>(builder: (context, child, val) {
return EasyRefresh(
enableControlFinishLoad: true,
controller: _controller,
child: ListView(
shrinkWrap: true,
children: <Widget>[
HomeSearchBar(),
HomeBanner(),
HomeTopNavi(),
HomeGroup(),
HomeTimeBuy(),
HomeRecommend()
],
),
onLoad: () async {
HomeProvide homeProvide = Provide.value<HomeProvide>(context);
await homeProvide.getRecommendGoods();
_controller.finishLoad(
success: true, noMore: !homeProvide.recGoodsModel.hasMore);
},
footer: ClassicalFooter(
bgColor: Colors.white,
textColor: Colors.black,
infoColor: Colors.black,
),
);
});
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
));
}
_getHomeData(context) async {
await Provide.value<HomeProvide>(context).getHomeData();
return '完成';
}
}
主页大致就是以上这些内容了,还有一些小程序的小元素没实现,比较懒,有空补上,下一篇我会完成分类页面。
第一篇链接:flutter仿网易严选(一)
第二篇链接:flutter仿网易严选(二)
第三篇链接:flutter仿网易严选(三)
第四篇链接:flutter仿网易严选(四)