美文网首页
native, react native, flutter ap

native, react native, flutter ap

作者: 码山打虎 | 来源:发表于2019-10-22 19:07 被阅读0次
    image.png

    前端开发是离用户最近的工程领域,需要在开发时间和体验上不断作出选择和权衡, 就像著名的论断 “php是最好的计算机语言"一样, js也能依靠(node, react native)一统天下. 我们都想要一个统一的框架搞定一切.

    而目前的情况是即便是同一个app的界面, 我们也在糅合这些不同的框架, 用来快速迭代,适应变化。最近抽了点时间把app开发领域人气比较高的框架凑到了一块而,对比体会了一下,其间也有一些小的收获。

    缘起reactive

    首先对于这些框架解决的根本问题, 我特意查了下他们官网的简介, flutter我想将它称之为reactive app, 加上react native和native 从三者的名字上看就知到reactive有多重要了. 这些年reactive面向数据流的开发趋向一直是主流, 而react 开发思想最早是从microsoft的c#语言中涌现出来的, dart和typescript是后面我将会使用的2种语言, 它们都借鉴了c#语言的优秀设计思想.

    废话不多说, 这次实验实现了实时搜索并动态展示图片的简单单页面app, 先看下这次实现的app在平台上最终的效果, 没有UI设计,没有适配(轻喷)

    release版本实现情况

    解释下这个单页面app实现的原则, 在使用reactive开发模式的前提下, 除了必须的平台上基本的http框架,本次实现尽量或者避免使用第三方库.

    native平台

    首先是native 平台, kotlin实现, 使用了recyclerview+okhttp, 没有用到任何图片加载框架

    虽然recyclerview为我们的gridview item提供了回收机制, 图片和view的绑定及其生命周期的处理都需要格外注意:

            (search as EditText).addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    val url = "https://anime-pictures.net/pictures/view_posts/0?&lang=zh_CN&search_tag=${s.toString()}"
                    client.newCall(Request.Builder().get().url(url).build())
                        .enqueue(object: Callback {
                            override fun onFailure(call: Call, e: IOException) {
                                e.printStackTrace()
                            }
                            override fun onResponse(call: Call, response: Response) {
                                var html = response.body?.string()
                                println(html)
                                imageList.clear()
                                imageList.addAll(Regex("img_cp[\\s\\S]+?src=\"//([\\s\\S]+?)\"").findAll(
                                    html.toString()
                                )
                                    .map {
                                        it.groupValues[1]
                                    }
                                    .toList())
                                println(imageList)
                                runOnUiThread { grid.adapter?.notifyDataSetChanged() }
                            }
                        })
                }
    
                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
                }
    
                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                }
            })
        }
    
        class GalleryAdapter(
            private val imageList: LinkedList<String>,
            private val client: OkHttpClient
        ) : RecyclerView.Adapter<GalleryAdapter.GalleryViewHolder>() {
            private var loadings: HashMap<String, Bitmap?> = HashMap()
    
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder {
                val item : View = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
                registerAdapterDataObserver(object: AdapterDataObserver(){
                    override fun onChanged() {
                        loadings = HashMap()
                    }
                })
                return GalleryViewHolder(item)
    }
                val url = imageList[position]
                holder.itemView.image.tag = url
                val imageRef : WeakReference<ImageView> = WeakReference(holder.itemView.image)
                if(loadings[url] != null){
                    holder.itemView.image.setImageBitmap(loadings[url])
                }else {
                    client.newCall(Request.Builder().get().url("https://$url").build())
                        .enqueue(object : Callback {
                            override fun onFailure(call: Call, e: IOException) {
                                e.printStackTrace()
                            }
    
                            override fun onResponse(call: Call, response: Response) {
                                val image = imageRef.get()
                                println("done:$url")
                                if(image != null && image.tag == url) {
                                    val input: InputStream? = response.body?.byteStream()
                                    if(input != null) {
                                        val bitmap: Bitmap =
                                            BitmapFactory.decodeStream(input)
                                        input.closeQuietly()
                                        loadings[url] = bitmap
                                        holder.itemView.post {
                                            holder.itemView.image.setImageBitmap(bitmap)
                                        }
                                    }
                                }else if(image != null && image.tag != url){
                                    //imageview reused
                                    if(loadings[image.tag] != null){
                                        holder.itemView.post {
                                            holder.itemView.image.setImageBitmap(loadings[image.tag])
                                        }
                                    }
                                }
                            }
                        })
    

    这里面展示了其中整个app的核心代码逻辑, 由于没有使用图片加载框架, bitmap存到了一个临时的hashmap中和url绑定, 由于设置图片时时完全异步的, 需要对imageView的回收和复用状态做出判断,这里使用了weakreference和image.tag数据绑定作判断。要写一个做的好的话还要实现threadpool task cancel和memory diskcache等操作, 这里略去n行代码。 native app实现大约140行,如果引入glide等图片框架代码减少到120行左右,但如果加上layout xml代码40行,考虑都是可见即所得的设计算作10行kotlin的话计作 150行, native不出意料应该代码最多,且往后看。

    flutter框架

    flutter平台的app实现最终如下,

    import 'package:flutter/material.dart';
    import 'package:http/http.dart' as http;
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'reactive'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      List<String> images = [];
      int loadings = 0;
      DateTime nows = DateTime.now();
      get serviceList => () {
            return images.map((url) => watch_image(url)).toList();
          };
    
      Image watch_image(String url) {
        NetworkImage image = NetworkImage("https://" + url);
    
        image
            .resolve(new ImageConfiguration())
            .addListener(ImageStreamListener(_handleResolve));
        return Image(
          image: image,
          height: 150.0,
        );
      }
    
      void _handleResolve(ImageInfo image, bool synchronousCall) {
        print(loadings);
        if (mounted && loadings > 0) {
          loadings = loadings - 1;
          if (loadings == 0) {
            print("addPostFrameCallback be invoke:" +
                (DateTime.now().second - nows.second).toString());
          }
        }
      }
    
      get onchange => (str) {
            http
                .get(
                    "https://anime-pictures.net/pictures/view_posts/0?order_by=views&ldate=0&lang=zh_CN&search_tag=" +
                        str) //TODO
                .then((response) {
              return RegExp(r'img_cp[\s\S]+?src="\/\/([\s\S]+?)"', multiLine: true)
                  .allMatches(response.body)
                  .map((mt) => mt.group(1))
                  .toList();
            }).then((List<String> urls) {
              print(urls);
              _updateWheel(urls);
            });
          };
    
      void _updateWheel(List<String> urls) {
        setState(() {
          images.clear();
          images.addAll(urls);
          loadings = urls.length;
    
          nows = DateTime.now();
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                Center(
                    child: TextField(
                  autofocus: true,
                  onChanged: onchange,
                  maxLines: 1,
                )),
                Flexible(
                  flex: 1,
                  child: GridView.count(
                      crossAxisCount: 2,
                      padding: EdgeInsets.symmetric(vertical: 0),
                      children: serviceList()),
                )
              ]),
        );
      }
    }
    
        需要注意的时, 由于是实时搜索动态刷新, 需要用到stateful widget,   flutter提供的 networkimage 控件相当实用, 既简化了native中图片加载的各种缓存,又默认了最佳的display options实现起来相当高效,  其正则表达式的语法和kotlin非常相像,这里抓取的是[https://anime-pictures.net](https://anime-pictures.net/)主页的动漫图片, 正则规定了首尾的token, 最终截取group index是1的字符串.  Flutter框架整个app算上import 总共110行左右, 真实体验是代码少了,自己需要考虑的逻辑也少了不少。
    

    react native框架

    最后是最让大家感觉是个谜团的框架, 从实现机制上来说,reactnative还是需要把js的绘制指令翻译成native对应的指令才可以实现逻辑, 而flutter的reactive app是生成了gpu和cpu相关的机器绘制指令,在ui渲染这块应该很占优势才对, 下面。。。

    import React, {Component} from 'react';
    import {FlatList, StyleSheet, Text, View, Image, TextInput} from 'react-native';
    
    export default function App() {
        return (<SearchGridComponent/>)
    }
    
    interface IProps {
      url: String
    }
    
    class SearchGridComponent extends Component{
        constructor(props) {
            super(props);
            this.state = {
                dataArray: []
            }
        }
    
        onChangeText = (text) => {
            fetch("https://anime-pictures.net/pictures/view_posts/0?order_by=views&ldate=0&lang=zh_CN&search_tag=" + text,
                {
                    method: "get",
                    headers: {
                        "mode": "no-cors"
                    }
                })
                .then((response) => response.text())
                .then((text) => {
                    console.log("seach result:" + text)
                    var items = RegExp("img_cp[\\s\\S]+?src=\"//([\\s\\S]+?)\"", "gi")
                        [Symbol.match](text)
                        .flatMap((item) =>
                        {
                            return {url: "https:" + (item.substring(item.indexOf("//"), item.lastIndexOf("\"")))}
                        })
                    this.setState({dataArray:items})
                })
                .catch((error) => {
                    console.warn(error);
                });
        }
    
        render(){
            console.log(this.state["dataArray"])
            return (
                <View>
                    <Text style={styles.texts}>React Native</Text>
                    <TextInput onChangeText={this.onChangeText}
                            style={styles.inputs}/>
                    <FlatList
                        data={this.state["dataArray"]}
                        extraData={this.state}
                        numColumns ={2}
                        keyExtractor={(item, index) => item.url}
                        renderItem={({item})=><ImageList url={item.url}/>}
                    />
                </View>
            )
        }
    
    }
    
    class ImageList extends Component<IProps>{
        constructor(props) {
            super(props);
        }
        render() {
            return (
                <Image source={{uri:this.props.url}} style={styles.img}/>
            )
        }
    }
    
    const styles = StyleSheet.create({
        container: {
            flexDirection: 'row',
            padding: 5,
        },
        texts:{
            width: "100%",
            height: 48,
            textAlign:'center',
            alignItems:'center',
            justifyContent:'center',
            textAlignVertical:'center',
        },
        inputs:{
            width: "100%",
            height: 40,
            backgroundColor: "#f1f2f3"
      },
      img: {
        width: "50%",
        height: 180,
      },
    })
    

    刚好到100行! 我没有特意优化什么, 都是用react的思想去实现动态交互式布局的, 如果用js实现应该比这个还少,向component的props typescript还需要定义接口, 不过这个也让代码自动化和补全有了想象空间, 论ui实现的开发效率, typescript/js 略胜flutter一筹, 10%吧 ;)其实,不过这里我要吐槽一下:

    • 环境配置方面:

    reactnative 对我这个js生手来说语法啥的到容易,环境搭建,兼容性问题委实费了我一些功夫, 首先是js debug时的跨域问题, cross-origin( 这个是浏览器对运行在其页面的js限制, 在真机上debug时还是需要开代理才能绕过)

    • 框架文档一致性:

    然后是reactnative expo插件, 这个在debug时是没事的, 我用的官方给的示例demo, 最后原生编译时androidx的库都找不到, 最终是通过jetifier代码反射,修改解决的, npm生态下的安装包和插件也是够全的, 官方demo更新不积极, 但是community的热情还是不错的, 对比而言flutter在文档上比reactnative厉害一些。

    • 框架api方面:

    这个我没有深入去研究, 其中一个比较坑的地方就是flat component更新界面需要state + extraData同时设置才可以, 另外js的正则还是很弱, 没有extract group的功能,这里只能字符串处理来解决。虽然作为js的超集,typescript/js的语言函数api虽然繁多,但是设计和实用性还是有待考量的。

    • 历史遗留问题:

    不同平台底层兼容性和相对为人诟病的内存处理机制(特别是复杂项目)

    各方面对比一览:

    1.开发效率/代码行数:

    native(kotlin): flutter: reactnative(typescript) >150: ~110: <100

    2.文档一致性(严谨自洽):

    native(kotlin)≥ flutter > reactnative(typescript)

    3.平均使用内存:
    由于每个平台对图片作了自己的缓存和优化策略, 所以直接比较不合理, 我们从系统资源使用的角度去考量用户的UI使用体验, 这里考虑平均使用内存:

    平均内存占用

    native(2.8) << flutter(9.1) ~≥ reactnative(8.8)

    1. 使用体验,性能方面
      由于动图太大,就不上了。 每个网络加载和缓存默认策略不同, reactnative默认加载平滑一些, flutter默认图片加载的先后顺序有些慢, native默认网络加载偏慢,这块可以有极大的优化空间.

    总结

    综合以上各方面,以上只是实验性的检验, 具体机器上可能会有差异, 在具体开发的时候需要根据具体场景做出判断, native在资源使用上还是有无可比拟的优势, 开发效率上就另说了。 reactnative坑多,但是开发总体简单, 在追赶reactnative开发体验的同时, flutter平台对硬件性能的压榨又更进一步了。

    相关文章

      网友评论

          本文标题:native, react native, flutter ap

          本文链接:https://www.haomeiwen.com/subject/agjqvctx.html