CSDN ·

native, react native, flutter app开发框架对比

前端开发是离用户最近的工程领域,需要在开发时间和体验上不断作出选择和权衡, 就像著名的论断 “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设计,没有适配(轻喷)

解释下这个单页面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])
 }
 }
 }
 }
 })
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

这里面展示了其中整个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()),
 )
 ]),
 );
 }
}
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111

需要注意的时, 由于是实时搜索动态刷新, 需要用到stateful widget, flutter提供的 networkimage 控件相当实用, 既简化了native中图片加载的各种缓存,又默认了最佳的display options实现起来相当高效, 其正则表达式的语法和kotlin非常相像,这里抓取的是 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,
 },
})
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

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

总结

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

原文:blog.csdn.net。 这里是静态副本,平台改了、删了,这一份还在。

← CSDN 全部 5 篇