首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  程序员

Flutter 三探

  •  2
     
  •   pjhubs · 94 天前 · 4143 次点击
    这是一个创建于 94 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址:PJ 的 iOS 开发之路

    历时一个星期对 Flutter 一期调研在这篇文章中就告一段落了,这篇文章中继续完善上篇文章中利用豆瓣电影 Top250 公开 API demo。

    前言

    在这两三天的继续完善 demo 的时间中,首先是对 Flutter 在基本 UI 视觉方面的实现表示赞赏,有些地方的 UI 布局的代码编写习惯了 Flutter 的思维后,会有一个非常快速的反应。下面是具体 demo 具体的完成图:

    首页.png

    下拉刷新.png

    详情.png

    整体 demo 做完后,全程都是在使用 meizu 15 这台开发机进行调试,在 flutter 的 IDE 选择上一直都在使用 Android Studio,在断点调试、查看渲染节点、性能对比等活动上都非常方便的解决了,依然强推!但整体没有遵循 Flutter 官方推荐的 BloC 设计模式,还是采用“设计模式之王”的 MVC,同样是考虑到了后续在对比其它跨段方案时尽可能的保证一致性。

    数据来源

    豆瓣电影详情 API 同样不需要做验证,传入对应电影的 id 即可,但会限制同一 IP 在一定间隔时间内的访问次数,如果在一定间隔时间内容访问 API 的速度过于频繁,则会拒绝服务,不过得益于 Flutter 的 hot reload 技术,可以不需要每次都重新拉去数据。该详情 API 多了一些更具体的数据,但依然没有达到豆瓣 App 本身那般丰富。

    《神秘巨星》电影详情数据.png

    涉及 Flutter 知识点

    • 下拉刷新;
    • 上拉加载;
    • 利用 GestureDetector Widget 进行页面跳转(动态路由方式);
    • 利用 SingleChildScrollView Widget 进行滚动视图的构建;
    • 简单性能分析。

    实践

    目录结构

    目录结构.png

    数据处理

    电影详情 API 返回的字段更多,同样可以确认的是 Model 也一定要从网络数据源中进行抛离,这同样也为后续构建子组件回填数据时提供方便,我的电影详情 Model 如下所示:

    class MovieMember {
      String id;
      // 成员姓名
      String name;
      // 详情 URL
      String detailUrl;
      // 中清晰度头像
      String avatarUrl;
    
      MovieMember({
        this.name,
        this.detailUrl,
        this.avatarUrl,
      });
    
      MovieMember.fromJSON(Map<String, dynamic> json) {
        this.id = json['id'];
        this.avatarUrl = json['avatars']['medium'];
        this.detailUrl = json['alt'];
        this.name = json['name'];
      }
    }
    
    class MovieDetail {
      // 标题
      String title;
      // 上映年份
      String year;
      // 原名
      String originalTitle;
      // 所属国家或地区
      List<String> countries;
      // 评分
      String rating;
      // "想看"人数
      String wishCount;
      // 星星
      String stars;
      // 高清晰度海报
      String poster;
      // 电影类型
      List<String> genres;
      // 评分人数
      int ratingsCount;
      // 主要导演
      List<MovieMember> director;
      // 主要演员
      List<MovieMember> casts;
      // 简介
      String summary;
    
      MovieDetail({
        this.title,
        this.year,
        this.countries,
        this.rating,
        this.stars,
        this.poster,
        this.genres,
        this.ratingsCount,
        this.director,
        this.casts,
        this.summary,
        this.wishCount,
      });
    
      MovieDetail.fromJSON(Map<String, dynamic> json) {
        this.title = json['title'];
        this.year = json['year'];
        this.summary = json['summary'];
        this.poster = json['images']['large'];
        this.ratingsCount = json['ratings_count'];
        this.originalTitle = json['original_title'];
        this.wishCount = json['wish_count'].toString();
    
        this.rating = json['rating']['average'].toString();
        this.stars = json['rating']['stars'].toString();
    
        this.countries = new List<String>.from(json['countries']);
        this.genres = new List<String>.from(json['genres']);
    
        List<MovieMember> castsMembers = [];
        (json['directors'] as List).forEach((item) {
          MovieMember movieMember = MovieMember.fromJSON(item);
          castsMembers.add(movieMember);
        });
        this.director = castsMembers;
    
        List<MovieMember> directorMembers = [];
        (json['casts'] as List).forEach((item) {
          MovieMember movieMember = MovieMember.fromJSON(item);
          directorMembers.add(movieMember);
        });
        this.casts = directorMembers;
      }
    }
    

    在写 MovieDetail Model 时发现电影详情 API 返回数据源中的“演员”数据存在多字段必要数据,为了后续方便调用同样也抽离了一个 MovieMember Model (二期调研估计会继续做演员详情)。

    演员数据格式.png

    网络数据的获取因为有了上篇文章的铺垫,这次再写一个速度明显快了很多,一期调研完整的网络层方法如下:

    import 'dart:io';
    import 'dart:convert';
    import 'package:movie_top_250/Model/movieModel.dart';
    
    class MovieAPI {
      Future<Movies> getMovieList(int start) async {
        var client = HttpClient();
        int page = start * 50;
        var request = await client.getUrl(Uri.parse(
            'https://api.douban.com/v2/movie/top250?start=$page&count=50'));
        var response = await request.close();
        var responseBody = await response.transform(utf8.decoder).join();
        Map data = json.decode(responseBody);
        return Movies.fromJSON(data);
      }
    
      Future<MovieDetail> getMovieDetail(String movieId) async {
        var client = HttpClient();
        var request = await client.getUrl(Uri.parse(
            'https://api.douban.com/v2/movie/subject/$movieId'));
        var response = await request.close();
        var responseBody = await response.transform(utf8.decoder).join();
        Map data = json.decode(responseBody);
        return MovieDetail.fromJSON(data);
      }
    }
    

    下拉刷新与上拉加载

    下拉刷新的整体与 Native 开发思路一致。上拉加载有根据业务有很多种实现方案,因为只是为了做验证性 demo,直接采取了使用“静默加载”的思路,当然因为一次加载数据量太大(一页 50 条),所以在快速滑动列表时会导致下一页数据未载入等待的体验。如果不考虑过多的定制化操作,直接使用 flutter 系统组件是一件非常舒服的事情。完整代码如下:

    import 'package:flutter/material.dart';
    import 'package:movie_top_250/Service/movieApi.dart';
    import 'package:movie_top_250/Model/movieModel.dart';
    import 'package:movie_top_250/View/List/movieListViewRowWidget.dart';
    
    class MovieWidget extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _DouBanMovieState();
      }
    }
    
    class _DouBanMovieState extends State<MovieWidget> {
      // 数据源
      List<Movie> movies = [];
      // 分页
      int page = 0;
    
      @override
      void initState() {
        super.initState();
        _requestData();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('豆瓣电影 Top250'),
          ),
          body: new RefreshIndicator(
            child: _buildList(context),
            onRefresh: _requestData,
            color: Colors.black,
          ),
        );
      }
    
      // 下拉刷新
      Future<Null> _requestData() async {
        movies.clear();
        await MovieAPI().getMovieList(0).then((moviesData) {
          setState(() {
            movies = moviesData.movies;
          });
        });
        return;
      }
    
      // 上拉加载
      _requestMoreData(int page) {
        print('page = $page');
        MovieAPI().getMovieList(page).then((moviesData) {
          setState(() {
            movies += moviesData.movies;
          });
        });
      }
    
      // body List Widget
      Widget _buildList(BuildContext context) {
        var screenWidth = MediaQuery.of(context).size.width;
        if (movies.length != 0) {
          return ListView.separated(
              itemBuilder: (context, index) {
                // 还剩 15 条数据的时去拉取新数据
                if (movies.length - index == 15) {
                  _requestMoreData(++page);
                }
                return new Container(
                  width: screenWidth,
                  child: buildListRow(index, movies[index], context),
                );
              },
              separatorBuilder: (context, index) => Divider(
                    height: 1,
                  ),
              itemCount: movies.length);
        } else {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
      }
    }
    

    UI 分析

    上文中也已经说到,因为豆瓣电影详情公开 API 所暴露出的数据有限,导致未能 100% 的重写。经过分析后主要将页面分为了以下几部分:

    豆瓣电影详情 UI 布局分析

    第一部分

    第一部分与上篇文章中所讲述的布局编写思路大部分一致,对于我自己来说有个需要注意的地方,在第一部分中有个“豆瓣电影排名”的 badge,原本打算是用 RichText Widget 进行实现的,但翻完属性后发现并没有提供 decoration 字段进行修饰,最后直接使用了两个 DecoratedBox Widget 作为父容器,在其 decoration 属性下使用 BoxDecoration Widget 完成“一左一右”圆角的 badge 组件编写,flutter 在组件“半圆角”的实现过程比 iOS 原生实现的代码量上少太多了(不封装的话),实现代码如下:

    Widget _buildBadge(int index, MovieDetail movieDetail) {
      index++;
      return new Row(
        children: <Widget>[
          DecoratedBox(
              child: Padding(
                  padding: EdgeInsets.fromLTRB(7, 3, 7, 3),
                  child: Text('No.$index',
                      style: TextStyle(color: Colors.brown, fontSize: 14))),
              decoration: new BoxDecoration(
                  color: Colors.orangeAccent,
                  borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(5),
                      bottomLeft: Radius.circular(5)))),
          DecoratedBox(
              child: Padding(
                  padding: EdgeInsets.fromLTRB(7, 3, 7, 3),
                  child: Text('豆瓣 Top250',
                      style: TextStyle(color: Colors.orangeAccent, fontSize: 12))),
              decoration: new BoxDecoration(
                  color: Colors.black45,
                  borderRadius: BorderRadius.only(
                      topRight: Radius.circular(5),
                      bottomRight: Radius.circular(5)))),
        ],
      );
    }
    

    在实现“想看”和“看过”两个按钮组件时,我使用了 RaisedButton Widget 。一开始是这么写的:

    new RaisedButton(
      onPressed: null,
      color: Colors.white,
      child: new Text('B'),
      textColor: Colors.black,
    )
    

    当显示出来后,不管怎么调整样式、修改颜色、文字等都不管用。最后带着郁闷的心情浏览官方文档,居然发现了这么一段话:

    RaisedButton 官方解释

    嗯,就算我们并不想让这个 Button 响应任何点击事件也不能给这个属性置空,并且也不能删除,因为这是个必须参数......这点需要注意。让同样也没想到的是 RaisedButton 没有 text 或类似设置按钮文本的属性,而是给了一个 child 属性,被 UIButton 虐过几次后在 Flutter 中看到某个组件提供了 child 属性现在就两眼放光!完成的代码如下:

    Widget _buildButton() {
      return new Padding(
        padding: EdgeInsets.fromLTRB(0, 20, 0, 0),
        child: new Row(
          children: <Widget>[
            new Padding(
              padding: EdgeInsets.fromLTRB(0, 0, 10, 0),
              child: new RaisedButton(
                onPressed: () {},
                color: Colors.white,
                child: new Row(
                  children: <Widget>[
                    new Padding(
                      padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
                      child: new Icon(
                        Icons.remove_red_eye,
                        size: 18,
                        color: Colors.orange,
                      )
                    ),
                    new Text('想看',
                      style: new TextStyle(
                        color: Color.fromRGBO(100, 100, 100, 1),
                        fontSize: 16,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                  ],
                ),
                textColor: Colors.black,
              ),
            ),
            new RaisedButton(
              onPressed: () {},
              color: Colors.white,
              child: new Row(
                children: <Widget>[
                  new Padding(
                      padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
                      child: new Icon(
                        Icons.star_border,
                        size: 18,
                        color: Colors.orange,
                      )
                  ),
                  new Text('看过',
                    style: new TextStyle(
                      color: Color.fromRGBO(100, 100, 100, 1),
                      fontSize: 16,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ],
              ),
              textColor: Colors.black,
            ),
          ],
        ),
      );
    }
    

    第二部分

    这部分布局与上篇文章所讲述的内容都是一致的。并且我也偷懒了,主要是没有太多值得花费心思的地方,都是常规的布局.本来想实现下“进度条”,但无奈并没有真实数据,也就懒得弄了。完整代码如下:

    import 'package:flutter/material.dart';
    import 'package:movie_top_250/Model/movieModel.dart';
    
    
    Widget movieDetailStarWidget(MovieDetail movieDetail) {
      return new DecoratedBox(
        decoration: new BoxDecoration(
            color: Color.fromRGBO(65, 46, 37, 1),
            borderRadius: BorderRadius.all(Radius.circular(5))),
          child: new Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              new Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  new Padding(
                      padding: EdgeInsets.all(10),
                      child: new Text(
                        '豆瓣评分™',
                        style: TextStyle(color: Colors.white),
                      )
                  )
                ],
              ),
              _buildRatingStar(movieDetail),
            ],
          )
      );
    }
    
    Widget _buildRatingStar(MovieDetail movieDetail) {
      List<Widget> icons = [];
      int fS = int.parse(movieDetail.stars) ~/ 10;
      int f = 0;
    
      while (f < fS) {
        icons.add(new Icon(Icons.star, color: Colors.orange, size: 15));
        f++;
      }
      while (icons.length != 5) {
        icons.add(new Icon(Icons.star,
            color: Color.fromRGBO(220, 220, 220, 1), size: 15));
      }
    
      return new Padding(
        padding: EdgeInsets.fromLTRB(0, 5, 0, 10),
        child: new Column(children: <Widget>[
          new Padding(
            padding: EdgeInsets.fromLTRB(5, 0, 0, 10),
            child: new Text(
              movieDetail.rating,
              style: new TextStyle(
                fontSize: 35,
                fontWeight: FontWeight.w500,
                color: Color.fromRGBO(220, 220, 220, 1),
              ),
            ),
          ),
          new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: icons
          ),
        ]),
      );
    }
    

    第三部分

    这部分涉及到了长文本,flutter 中同样也没有提供长文本显示组件,但好在 flutter 的 Text Widget 本身就适用于长文本展示的组件,默认开启 softWrap 属性(自动换行)。Text Widget 会从自身节点树里一直向上寻找能够提供宽度约束的父组件,并以此作为单行文本最长显示宽度,这点还是比较惊讶的,省了非常多的事情。完整的代码如下:

    import 'package:flutter/material.dart';
    import 'package:movie_top_250/Model/movieModel.dart';
    
    Widget movieDetailSummaryWidget(MovieDetail movieDetail) {
      return new Padding(
        padding: EdgeInsets.all(15),
        child: new Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            new Padding(
              padding: EdgeInsets.fromLTRB(0, 0, 0, 10),
              child: new Text(
                '简介',
                style: new TextStyle(
                  fontSize: 20,
                  color: Colors.white,
                  fontWeight: FontWeight.w600,
                ),
              )
            ),
            new Text(
              movieDetail.summary,
              style: new TextStyle(
                  color: Colors.white,
                  fontSize: 15,
              )
            )
          ],
        )
      );
    }
    

    当数据展示出来后,发现超出当前页面的显示范围了,这也是意料之中。按照之前的做法,会把 UIScrollView 作为当前页面所有原始的父容器,等所有 UI 元素都回填数据重新渲染完后,再把位于最底部的 UI 组件 bottom 值赋给 scrollView.contentSize

    翻了 flutter 文档后,发现了提供常规滑动视图能力的 Widget 不只一个,最后选择了 SingleChildScrollView。本以为设置滑动区域的步骤也会向在 Native 中那般麻烦,但实际上只需要把需要滑动视图组件的父节点赋给 child 属性即可,SingleChildScrollView 同样会自动的拓展自己的滑动区域进行适配,如下所示:

    Widget _buildBody(BuildContext context) {
        // 数据源没来时展示 loading
        if (movieDetail == null) {
          return new Center(
            child: new CircularProgressIndicator(),
          );
        } else {
          return new SingleChildScrollView(
            child: new Padding(
                padding: EdgeInsets.all(10),
                child: new Column(children: <Widget>[
                  movieDetailHeaderWidget(rankIndex, movieDetail, context),
                  movieDetailStarWidget(movieDetail),
                  movieDetailSummaryWidget(movieDetail),
                  movieDetailMemberWidget(movieDetail),
                ])
            ),
          );
        }
    }
    

    第四部分

    这部分是整体比较纠结的地方,到底是基于 GridView Widget 还是 SingleChildScrollView Widget 配合着其它布局 Widget 去做呢?如果这在 iOS 中,我会毫不犹豫的选择 UICollectionView 进行构建,因为又快又好~

    最后还是抱着“又快又好”目的出发,选择了 SingleChildScrollView Widget 配合着其它布局 Widget 去做。需要注意是的 SingleChildScrollView Widget 默认是纵向滚动,该部分豆瓣 App 进行的横行滚动,需要改变滚动视图方式。完整代码如下:

    import 'package:flutter/material.dart';
    import 'package:movie_top_250/Model/movieModel.dart';
    
    Widget movieDetailMemberWidget(MovieDetail movieDetail) {
      List<Widget> memberWidgets = [];
    
      for (MovieMember member in movieDetail.director) {
        memberWidgets.add(_buildMemberWidget(member, true));
      }
    
      for (MovieMember member in movieDetail.casts) {
        memberWidgets.add(_buildMemberWidget(member, false));
      }
    
      return new SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: new Row(
          children: memberWidgets,
        ),
      );
    }
    
    Widget _buildMemberWidget(MovieMember member, bool isDirector) {
      var col = new Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          new Container(
            width: 110,
            height: 160,
            decoration: new BoxDecoration(
              image: DecorationImage(image: NetworkImage(member.avatarUrl)),
              borderRadius: new BorderRadius.all(
                const Radius.circular(8.0),
              ),
            ),
          ),
          new Text(member.name,
            style: new TextStyle(
                color: Colors.white,
            ),
          ),
        ],
      );
    
    
      if (isDirector) {
        col.children.add(
          new Text(
            '导演',
            style: new TextStyle(
                fontSize: 13,
                color: Color.fromRGBO(150, 150, 150, 1)
            ),
          )
        );
      } else {
        col.children.add(
            new Text(
              '演员',
              style: new TextStyle(
                  fontSize: 13,
                  color: Color.fromRGBO(150, 150, 150, 1)
              ),
            )
        );
      }
    
      return new Container(
        width: 110,
        child: new Padding(
          padding: EdgeInsets.all(15),
          child: col,
        ),
      );
    }
    

    分析工具

    在本 demo 中的 ListView Widget 未做太多优化的地方,所以会导致在启动 App 完成后直接开始上拉页面会体验到卡顿,但实际上的做法是先把当前页数据源的条数给 ListView 设置上,当用户停止滑动页面后,再开始 load 当前在可视区域范围内的 ListViewRow Widget 相关子组件数据。这一点优化在 iOS 上通过 UITableView 配合 RunLoop 就可以解决,但在对 flutter 的一期调研中并未打算开始此项调优工作。

    使用各种跨平台工具最令人感到窒息的莫过于调试了,基于 JSCore 比如 WeexReact-Native 等框架还行,能够利用 web 开发者工具搭配进行。但比如 XamarinQt 等框架想要进行调试基本上就比较费劲了,如果框架开发者不提供一些功能完备的 IDE 或插件,调试几乎等于噩梦。好在 Android Stuido 对 flutter 的支持是相当丰富,具体见下图:

    查看当前页面节点树

    分析工具

    分析工具.png

    性能相关(部分)

    其它

    webView

    今天原本还想做跳转 webView,以为也只是直接调个 webView Widget,填入 requestUrl 属性就完事了。但当我输入 web,IDE 并未提示任何相关信息时,开始发觉有点不太对劲,不会 flutter 没有提供 webView Widget 吧?仔细浏览后,确认 flutter 还真的没有提供官方 webView 组件,但在 Pub 上已经有了对应的插件。

    接着去掘金的 flutter 交流群里咨询,讨论在 flutter 中 webView 以及 JSBridge 最佳思路,最后讨论出了两个插件:

    对于 webView 这块不是特别满意,而且看了 flutter 在 github 上的 issue,推荐自己做一个 webView plugin,暴露给 flutter 进行调用,这样可以最大程度上的降低基础组件重写成本。仔细一想,其实还是不满意,所以这部分内容也延后到二期调研中了。

    StatefulWidgetStatelessWidget 的选择

    对于开发一个新的组件时,到底是基于 StatefulWidget 还是 StatelessWidget ,我认为只需要明确两个概念即可:

    • 是否需要更新组件数据源;
    • 是否需要利用组件各种生命周期;

    如果以上两个条件都符合,那就选择 StatefulWidget

    总结

    源码地址

    本次 flutter 一期调研学习产出的豆瓣电影 Top250 demo 链接地址:movie_top_250

    flutter 的这种“声明式”编码体验,我认为对于第一次接触的新手来说,肯定有需要一定的学习成本,当逼迫自己去熟悉开发思路后,就会觉得真的很过瘾。在一期调研的学习中,没有涉及到的方面有:

    • 设计模式;
    • webViewJSBridge
    • flutter 与 native 的交互;
    • 复杂 UI 的构建;
    • 音视频处理(这还是得通过 native 进行暴露);

    以上几块是构建一个 App 所需要具备的基本组件。经过一期学习后,对 flutter 也有了自己的理解,给我最大的感受是因为不用像 React-NativeWeex 等需要回调节点树给中间层通知 native 利用原生 UI 框架进行渲染,首先在 UI 绘制速度上已经远超其它框架,这点是毋庸置疑的。但因为 flutter 还太年轻,一些基础设施和社区都做得不算太好。所以如果非要选择一个跨端技术投入实际开发中,我还是会选择 React-Native,所以将重新捡起来 React-Native, 对其同样重新进行一期调研。

    43 回复  |  直到 2019-01-18 17:35:24 +08:00
        1
    faywong8888   94 天前
    👍,火钳刘明
        2
    w4mxl   94 天前
    👏,感谢分享~
        3
    yqrm   94 天前
    “所以如果非要选择一个跨端技术投入实际开发中,我还是会选择 React-Native,所以将重新捡起来 React-Native, 对其同样重新进行一期调研。”
        4
    deathscythe   94 天前
    0.5x 版本时候开始接触 Flutter, 感觉自己还没写出像样的东西, 失礼失礼
        5
    hilbertz   94 天前
    webview 一桶江湖
        6
    tcpdump   94 天前
    回复标记一下
        7
    SouthCityCowBoy   94 天前
    战略性马克下
        8
    66beta   94 天前 via Android
    楼主觉得没接触过 native 开发的,写出这个要多久?
        9
    deathscythe   94 天前
    楼主在 iOS 长列表卡顿问题是不是没有跑 release 版本?
        10
    avichen   94 天前
    不明觉厉
        11
    pjhubs   94 天前 via Android   ♥ 1
    @66beta flutter 因为抛弃了与 native 相关的 UI 框架,所以简单 App 可以不需要学习 native 相关内容,到 App 设计到需要调用原生功能,就需要会写 native 代码,以 plugin 暴露给 flutter。
        12
    pjhubs   94 天前 via Android
    @deathscythe 对,后来跑了,真是惊艳。
        13
    CommandZi   94 天前
    flutter 对于我来说,两点不舒服:一是 dart 的书写方式,二是 scrollView 的滚动摩擦系数和原生的不一致(这个 android 基本一样,iOS 上真是难受)
        14
    ihainan   94 天前
    相同的代码,在我的 iPhone X 上很流畅,跑到 Xperia X 上(都是 release mode,虽然是老机器但就调 API 拉个 20 条数据显示而已)就卡成翔,很谜。
        15
    RCissac   94 天前
    想问下 Android 和 IOS 代码复用率高不高啊,同一套代码 Android 和 IOS 差别大不大,最近也准备尝试下 flutter
        16
    learnshare   94 天前
    支持
    中文段落部分最好两端对齐
        17
    pjhubs   94 天前 via Android
    @CommandZi 第一点赞同,刚开始跟你一样特别难受,过了那两天就好了,现在觉得还挺舒服哈哈哈。

    第二点我还没感受到
        18
    pjhubs   94 天前 via Android
    @RCissac 我写的这个 demo 只有 800 行,没有写过任何一行 native 代码。
        19
    pjhubs   94 天前 via Android
    @ihainan 我 Android 的开发设备是骁 660,很流畅。
        20
    CommandZi   94 天前
    @pjhubs 「第二点我还没感受到」说明你并不常用 iOS。
    拿闲鱼来说,在首页列表用手指向上甩(单次),心里会有一个预期的滚动距离、停止位置。点进详情页面,同样的力度手指向上甩,那个滚动距离和停止位置会不一样的。
        21
    youngxu   94 天前 via Android
    正在学习 flutter,完全小白,mark
        22
    wobuhuicode   94 天前
    如果非要做一个跨平台的。我选择在自家产品内部搭建类小程序的。
        23
    zlgodpig   93 天前
    赞,留名
        24
    janxin   93 天前
    @66beta 也挺快的,大概没接触过 3 天基本上手吧,不过要看你对基本概念的接受程度
        25
    janxin   93 天前
    RN 除了热更新,有个好处国内避不开的就是一定要用 Webview,方便做一些更新...

    Flutter 用 Webview 现在真的是揪心
        26
    pjhubs   93 天前 via Android
    @CommandZi 我这是一期调研,针对初体验,是否值得一用,至于你说的部分因为在我写的 demo 中影响不大甚至根本没有你说的情况,所以我还没感受到。

    另外,我是滴滴出行的 iOS 研发工程师。
        27
    pjhubs   93 天前 via Android
    @janxin flutter 的热更新和 webView 还没有最佳解决方案吧。
        28
    pjhubs   93 天前 via Android
    @CommandZi 我觉得还有可能是因为咸鱼自己的原因,万一就是这么设计的呢对吧,我这项目代码都已开源了,我个人体验是非常流畅的,至于咸鱼是什么情况,代码我们也没看见怎么写的,没法一棍子打死。
        29
    StargazerWikiv   93 天前
    春节打算学一学 Flutter,谢谢楼主的分享。Mark !
        30
    CommandZi   93 天前
    @pjhubs
    iOS 研发工程师能导出是常用 iOS 吗? Facebook 开发 Android 版的工程师里用 iPhone 的不少。
    滚动摩擦系数不一致能导出某一个不流畅吗?我可是从来没说过 Flutter 不流畅。
    咸鱼自己的原因?除非淘宝对 Flutter 项目分叉并做了对应修改。
    「没法一棍子打死」?我更加不认同,我也是从来没有否定 Flutter,我也做个 Flutter Demo,各种原因没有继续而已。
        31
    beyoung   93 天前 via iPhone
    @pjhubs webview 现在还不不完善 这是是很大的硬伤
        32
    janxin   93 天前
    @pjhubs 热更新咸鱼说要开源,这个可以等一下看看
    Webview 官方做了一个,太基础了,还得等一段时间完善
        33
    denano   93 天前
    马克一记
        34
    pjhubs   93 天前 via Android
    @CommandZi 不是很懂你的逻辑,你赢了。
        35
    pjhubs   93 天前 via Android
    @janxin 期待
        36
    vicvinc   93 天前
    题主对 Weex 怎么看呢
        37
    pjhubs   93 天前 via Android
    @vicvinc weex 年末的时候也做了调研,具体详情可见:https://github.com/windstormeye/iOS-Course/blob/master/Weex/Weex%E6%96%B0%E6%89%8B%E8%AE%B0.md

    我的个人意见,如果团队内部偏前端多一些,且技术栈为 vue 全家桶,不用考虑直接上 weex。如果不是 vue 技术栈,可以再调研调研 rn 和 weex。

    如果团队偏 native 对一些,想做跨端减少人力,可以先用 flutter 切入一些简单页面,太复杂的需要再定夺。
        38
    mosaki   93 天前
    感谢楼主分享。希望有空也能撸下 flutter
        39
    Pastsong   93 天前
    @vicvinc Weex is dead...
        40
    liufish   93 天前
    感谢分享,支持支持!
    用 flutter 撸了个小 app https://play.google.com/store/apps/details?id=com.rustfisher.anime.nendaiki
        41
    vicvinc   93 天前
    @Pastsong dead+1
        42
    vicvinc   93 天前
    @pjhubs thx,目前在 RN 和 Flutter 之间考虑,投靠 RN 还是觉得成熟些,开发者份额比较多,文档周边都完善些
        43
    q951313970   93 天前
    标记
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2361 人在线   最高记录 4385   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 22ms · UTC 12:33 · PVG 20:33 · LAX 05:33 · JFK 08:33
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1