经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Flutter » 查看文章
如何使用Flutter开发一款电影APP详解
来源:jb51  时间:2019/7/22 10:25:49  对本文有异议

前言

使用Flutter开发一款App是一件非常愉快的事情,其出色的性能、跨多端以及数量众多的原生组件都是我们选择Flutter的理由!今天我们就来使用Flutter开发一款电影类的App,先看下App的截图。


从main.dart开始

在Flutter里main.dart是应用开始的地方:

  1. import 'package:flutter/material.dart';
  2. import 'package:movie/utils/router.dart' as router;
  3.  
  4. void main() => runApp(MyApp());
  5.  
  6. class MyApp extends StatelessWidget {
  7. // This widget is the root of your application.
  8. @override
  9. Widget build(BuildContext context) {
  10. return MaterialApp(
  11. debugShowCheckedModeBanner: false,
  12. title: '电影',
  13. theme: ThemeData(
  14. primarySwatch: Colors.blue,
  15. ),
  16. onGenerateRoute: router.generateRoute,
  17. initialRoute: '/',
  18. );
  19. }
  20. }

一般的,在Flutter中管理路由有两种方式,一种是直接使用Navigator.of(context).push(),这种方式比较适合非常简单的应用,随着应用的不断发展,逻辑越来越多,推荐使用具名路由来管理应用,本文也是使用的这种方式。直接将路由挂在MaterialApp的onGenerateRoute字段上即可,具体的路由定义放在了单独的文件中进行管理utils/router.dart:

  1. import 'package:flutter/material.dart';
  2. import 'package:movie/screens/home.dart';
  3. import 'package:movie/screens/detail.dart';
  4. import 'package:movie/screens/videoPlayer.dart';
  5.  
  6. Route<dynamic> generateRoute(RouteSettings settings) {
  7. switch (settings.name) {
  8. case '/':
  9. return MaterialPageRoute(builder: (context) => Home());
  10. case 'detail':
  11. var arguments = settings.arguments;
  12. return MaterialPageRoute(
  13. builder: (context) => MovieDetail(id: arguments));
  14. case 'video':
  15. var arguments = settings.arguments;
  16. return MaterialPageRoute(
  17. builder: (context) => VideoPage(url: arguments));
  18. default:
  19. return MaterialPageRoute(builder: (context) => Home());
  20. }
  21. }

真是像极了前端的路由定义,先将组件import进来,然后在各自的路由中return即可。

首页

在首页中使用TabBar来展示"正在热映"和"TOP250":

  1. import 'package:flutter/material.dart';
  2. import 'package:movie/screens/hot.dart';
  3.  
  4. class Home extends StatefulWidget {
  5. Home({Key key}) : super(key: key);
  6.  
  7. _HomeState createState() => _HomeState();
  8. }
  9.  
  10. class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
  11. TabController _tabController;
  12.  
  13. @override
  14. void initState() {
  15. super.initState();
  16. _tabController = TabController(vsync: this, initialIndex: 0, length: 2);
  17. }
  18.  
  19. @override
  20. Widget build(BuildContext context) {
  21. return Scaffold(
  22. appBar: AppBar(
  23. title: TabBar(
  24. controller: _tabController,
  25. tabs: <Widget>[
  26. Tab(text: '正在热映'),
  27. Tab(text: 'TOP250'),
  28. ],
  29. ),
  30. ),
  31. body: TabBarView(
  32. controller: _tabController,
  33. children: <Widget>[
  34. Hot(),
  35. Hot(history: true),
  36. ],
  37. ),
  38. );
  39. }
  40. }

两个页面的布局是一样的,只有数据是不同的,所以我们复用这个页面Hot,传入history参数来代表是否为Top250页面

复用的Hot组件

  • 在这个组件中,通过history字段来区分成两个页面。
  • 在页面initState的生命周期中,请求数据,再进行相应的展示。
  • 下拉刷新的功能是使用的RefreshIndicator组件,在其onRefresh中进行下拉时的逻辑处理。
  • Flutter没有直接提供上拉加载的组件,但是也是很容易实现,通过ListView的controller来做判断即可:当前滚动的位置是否到达最大滚动位置_scrollController.position.pixels == _scrollController.position.maxScrollExtent
  • 为了获得良好的用户体验,Tab来回切换的时候,我们不希望页面重新渲染,Flutter提供了混入类AutomaticKeepAliveClientMixin,重载wantKeepAlive即可,下面是完整的代码:
  1. import 'package:flutter/material.dart';
  2. import 'package:movie/utils/api.dart' as api;
  3. import 'package:movie/widgets/movieItem.dart';
  4.  
  5. class Hot extends StatefulWidget {
  6. final bool history;
  7. Hot({Key key, this.history = false}) : super(key: key);
  8.  
  9. _HotState createState() => _HotState();
  10. }
  11.  
  12. class _HotState extends State<Hot> with AutomaticKeepAliveClientMixin {
  13. List _movieList = [];
  14. int start = 0;
  15. int total = 0;
  16. ScrollController _scrollController = ScrollController();
  17.  
  18. @override
  19. void initState() {
  20. super.initState();
  21. _scrollController.addListener(() {
  22. if (_scrollController.position.pixels ==
  23. _scrollController.position.maxScrollExtent) {
  24. getMore();
  25. }
  26. });
  27. this.query(init: true);
  28. }
  29.  
  30. query({bool init = false}) async {
  31. Map res = await api.getMovieList(
  32. history: widget.history, start: init ? 0 : this.start);
  33. var start = res['start'];
  34. var total = res['total'];
  35. var subjects = res['subjects'];
  36. setState(() {
  37. if (init) {
  38. this._movieList = subjects;
  39. } else {
  40. this._movieList.addAll(subjects);
  41. }
  42. this.start = start + 10;
  43. this.total = total;
  44. });
  45. }
  46.  
  47. Future<Null> _onRefresh() async {
  48. await this.query(init: true);
  49. }
  50.  
  51. getMore() {
  52. if (start < total) {
  53. query();
  54. }
  55. }
  56.  
  57. @override
  58. bool get wantKeepAlive => true;
  59.  
  60. @override
  61. Widget build(BuildContext context) {
  62. super.build(context);
  63. return RefreshIndicator(
  64. onRefresh: _onRefresh,
  65. child: ListView.builder(
  66. controller: _scrollController,
  67. itemCount: this._movieList.length,
  68. itemBuilder: (BuildContext context, int index) =>
  69. MovieItem(data: this._movieList[index]),
  70. ),
  71. );
  72. }
  73. }

电影的详情页面

点击单条电影时使用Navigator.pushNamed(context, 'detail', arguments: data['id']);即可跳转详情页,在详情页中通过id再请求接口获取详情:

  1. import 'package:flutter/material.dart';
  2. import 'package:movie/widgets/detail/detailTop.dart';
  3. import 'package:movie/widgets/detail/rateing.dart';
  4. import 'package:movie/widgets/detail/actors.dart';
  5. import 'package:movie/widgets/detail/photos.dart';
  6. import 'package:movie/widgets/detail/comments.dart';
  7. import 'package:movie/utils/api.dart' as api;
  8.  
  9. class MovieDetail extends StatefulWidget {
  10. final id;
  11. MovieDetail({Key key, this.id}) : super(key: key);
  12.  
  13. _MovieDetailState createState() => _MovieDetailState();
  14. }
  15.  
  16. class _MovieDetailState extends State<MovieDetail> {
  17. var _data = {};
  18.  
  19. @override
  20. void initState() {
  21. super.initState();
  22. this.init();
  23. }
  24.  
  25. init() async {
  26. var res = await api.getMovieDetail(widget.id);
  27. setState(() {
  28. _data = res;
  29. });
  30. }
  31.  
  32. @override
  33. Widget build(BuildContext context) {
  34. return Scaffold(
  35. body: _data.isEmpty
  36. ? Center(child: CircularProgressIndicator(),)
  37. : SafeArea(
  38. child: Container(
  39. height: MediaQuery.of(context).size.height,
  40. width: MediaQuery.of(context).size.width,
  41. child: ListView(
  42. scrollDirection: Axis.vertical,
  43. children: <Widget>[
  44. MovieDetailTop(data: _data),
  45. Rate(count: _data['ratings_count'], rating: _data['rating']),
  46. Container(padding: EdgeInsets.all(10),child: Text(_data['summary'])),
  47. Actors(directors: _data['directors'], casts: _data['casts']),
  48. Photos(photos: _data['photos'],),
  49. Comments(comments: _data['popular_comments']),
  50. ],
  51. ),
  52. ),
  53. ),
  54. );
  55. }
  56. }

在详情页面中,我们封装了一些组件,这样能让项目更加容易阅读和维护,组件的具体实现就不详细介绍了,都是一些常用的原生组件,这些组件分别是:

  • widgets/detail/detailTop.dart 页面顶部的电影概述
  • widgets/detail/rateing.dart 评分组件
  • widgets/detail/actors.dart 演员表
  • widgets/detail/photos.dart 剧照
  • widgets/detail/comments.dart 评论组件

真实数据来自哪里?

应用中的数据都是从豆瓣开发者api中拉取的,分别是,正在热映in_theaters,top250top250和电影详情subject/id三个接口,请求这些接口是需要apikey的,为了大家能方便请求数据,我将apikey上传到了github上,还请大家温柔点,不要将这个apikey干爆了。

源码下载

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对w3xue的支持。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号