赞
踩
现在跨平台的框架主要有如下几种:
长期来看,跨平台开发一定会是一个趋势,因为其能带来如下好处:
减少开发成本,提升开发效率
动态部署,不依赖发版
但现阶段,框架很多,各有各的优缺点,对于应用开发的RD来说,面临一个框架如何选择的难题。在行业趋势没有真正出现之前,RD应该要勇于去学习,去尝试新框架,学习其设计思想,体验其优势与劣势,找到最适合自己的框架。
之前对Flutter做过简单应用的尝试(Flutter实现Git权限分配工具之旅),但不够深入,任何一个框架在没有真正进行深入实践时,根本无法判断其优缺点,为了不浮于表面,人云亦云的去判定Flutter框架,才有了这次的调研:基于Flutter实现58App的首页功能(首页模块是58App相对比较复杂的模块)
实现效果
在Flutter的Material Widget里,有BottomNavigationBar和TabBar两个类似的效果,但都无法直接使用,改造成本非常的大,最终选择自定义实现底部栏。
ImageButton的要求:
支持图片与文本
支持两种状态:default,active
不同状态有不同的图片,不同的文本颜色
实现思路:
InkResponse Widget实现处理点击事件
Column布局
StatelessWidget,通过props来修改状态
import 'package:flutter/material.dart';class ImageButton extends StatelessWidget {final double width;final double height;final String imageAssetName;final String activeImageAssetName;final GestureTapCallback onTap;final String text;final Color textColor;final Color activeTextColor;final bool isActive;const ImageButton({Key key,@required this.width,@required this.height,@required this.imageAssetName,@required this.activeImageAssetName,this.text,this.textColor,this.activeTextColor,this.onTap,@required this.isActive}) : super(key: key);@overrideWidget build(BuildContext context) {return InkResponse(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Image.asset(this.isActive ? this.activeImageAssetName : this.imageAssetName,width: width,height: height,fit: BoxFit.contain),Text(this.text,style: TextStyle(color: this.isActive ? this.activeTextColor : this.textColor),)],),onTap: onTap,);}}
要求:
tabItem数量为奇数,中间的发布大小凸出来
能与TabBarView联动
实现思路:
Container Widget设置高度,背景
Row,Expanded做等分
Padding设置每个tabItem的paddingTop
通过TabController实现与TabBarView联动
tabController 继承 ChangeNotifier,ChangeNotifier是用于通知观察机制
_controller.addListener()来监听TabBarView的切换
_controller.animateTo(i)来通知tab的切换
代码如下:
import 'package:flutter/material.dart';import 'package:flutter_gallery/wuba_demo/home/publish/publish_home.dart';import '../wuba_ui/button/image_button.dart';class NavigationItem {final String title;final String icon;final String activeIcon;NavigationItem({this.title,this.icon,this.activeIcon});}class HomeBottomNavigationBar extends StatefulWidget {final List<NavigationItem> items;final Function onTap;final TabController controller;final Color defaultColor;final Color selectColor;HomeBottomNavigationBar({@required this.items,this.onTap,@required this.controller,@required this.defaultColor,@required this.selectColor});@override_HomeBottomNavigationBarState createState() => _HomeBottomNavigationBarState();}class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {int _currentIndex;TabController _controller;@overridevoid initState() {super.initState();_updateTabController();}@overridevoid didUpdateWidget(HomeBottomNavigationBar oldWidget) {super.didUpdateWidget(oldWidget);_updateTabController();}@overridevoid dispose() {if (_controller != null) {_controller.removeListener(_handleTabControllerTick);}super.dispose();}void _handleTabControllerTick() {debugPrint('_handleTabControllerTick ${_controller.index}');if (this._currentIndex != _controller.index) {setState(() {this._currentIndex = _controller.index;});}}void _updateTabController() {if (widget.controller == _controller) {return;}// 移除老的controller的listenerif (_controller != null) {_controller.removeListener(_handleTabControllerTick);}_controller = widget.controller;if (_controller != null) {_controller.addListener(_handleTabControllerTick);_currentIndex = _controller.index;}}@overrideWidget build(BuildContext context) {var children = <Widget>[];// 添加正常的tab选项for (var i = 0; i < widget.items.length; i++) {var navigationItem = widget.items[i];children.add(Expanded(flex: 1,child: Padding(padding: EdgeInsets.only(top: 15),child: ImageButton(width: 23,height: 23,imageAssetName: navigationItem.icon,activeImageAssetName: navigationItem.activeIcon,text: navigationItem.title,textColor: widget.defaultColor,activeTextColor: widget.selectColor,isActive: this._currentIndex == i,onTap: () {if (this._controller != null) {this._controller.animateTo(i);}if (widget.onTap != null) {widget.onTap(i);}},),)));}// 添加发布itemchildren.insert(2, Expanded(flex: 1,child: ImageButton(width: 40,height: 40,imageAssetName: 'assets/images/home/wb_home_tab_publish_img.png',activeImageAssetName: '',text: '发布',textColor: widget.defaultColor,isActive: false,onTap: (){Navigator.push(context, PageRouteBuilder(transitionDuration: Duration(),pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation){return PublishHome();}));},),));return Container(height: 63,decoration: BoxDecoration(image: DecorationImage(image: AssetImage('assets/images/home/wb_tab_bg.png'),fit: BoxFit.fill)),child: Row(crossAxisAlignment: CrossAxisAlignment.stretch,children: children,));}}
实现思路:
Stack Positioned实现叠层布局,解决tabbar凸起部份覆盖在TabBarView上
TabBarView Widget实现类似ViewPager效果
代码如下:
import 'package:flutter/material.dart';import 'home_bottom_navigation_bar.dart';import 'package:wubarn_plugin/wuba_rn_view.dart';class HomeDemo extends StatefulWidget {static const String routeName = '/wuba/home';const HomeDemo({ Key key }) : super(key: key);@override_HomeDemoState createState() => _HomeDemoState();}class _HomeDemoState extends State<HomeDemo>with SingleTickerProviderStateMixin {List<NavigationItem> _navigationViews;TabController controller;@overridevoid initState() {super.initState();_navigationViews = <NavigationItem>[NavigationItem(icon: 'assets/images/home/wb_home_tap_index_normal.png',activeIcon: 'assets/images/home/wb_home_tap_index_pressed.png',title: '首页',),NavigationItem(icon: 'assets/images/home/wb_home_tap_history_normal.png',activeIcon: 'assets/images/home/wb_home_tap_history_pressed.png',title: '部落',),NavigationItem(icon: 'assets/images/home/wb_home_tap_message_normal.png',activeIcon: 'assets/images/home/wb_home_tap_message_pressed.png',title: '消息',),NavigationItem(icon: 'assets/images/home/wb_home_tap_center_normal.png',activeIcon: 'assets/images/home/wb_home_tap_center_pressed.png',title: '我的',)];controller = TabController(initialIndex: 2, length: this._navigationViews.length, vsync: this);}@overrideWidget build(BuildContext context) {return Scaffold(body: Stack(children: <Widget>[Positioned(top: 0,left: 0,right: 0,bottom: 50,child: TabBarView(controller: controller,children: <Widget>[Container(color: Colors.red,child: Text('Fragment'),),Container(child: WubaRNView(),),Container(color: Colors.white,child: Text('Fragment'),),Container(color: Colors.yellow,child: Text('Fragment'),)]),),Positioned(left: 0,right: 0,bottom: 0,height: 63,child: HomeBottomNavigationBar(items: this._navigationViews,controller: this.controller,defaultColor: Colors.black,selectColor: Colors.red,),)],),);}}
实现思路:
通过独立的Flutter Plugin实现
ReactNative的ReactRootView可以被嵌入Native中,那同样可以被嵌入Flutter中
Flutter的AndroidView只有两个状态:create,dispose。在这两个状态里,执行ReactNative相关的生命周期函数
dart部分:创建对应的Widget
class WubaRNView extends StatefulWidget {@override_WubaRNViewState createState() => _WubaRNViewState();}class _WubaRNViewState extends State<WubaRNView> {@overrideWidget build(BuildContext context) {// 不同的端,其通信方式不一样if (defaultTargetPlatform == TargetPlatform.android) {return AndroidView(viewType: 'plugins.wuba.com/wubarnview',onPlatformViewCreated: _onPlatformViewCreated,);}return Text('$defaultTargetPlatform is not yet supported by the WubaRNView plugin');}void _onPlatformViewCreated(int id) {}}
Android的实现:
注册ViewFactory
public class WubarnPlugin {public static final String VIEW_TYPE = "plugins.wuba.com/wubarnview";/** Plugin registration. */public static void registerWith(Registrar registrar) {registrar.platformViewRegistry().registerViewFactory(VIEW_TYPE, new WubarnViewFactory(registrar.messenger()));}}
通过ViewFactory创建WubarnView
public class WubarnViewFactory extends PlatformViewFactory {private final BinaryMessenger messenger;public WubarnViewFactory(BinaryMessenger messenger) {super(StandardMessageCodec.INSTANCE);this.messenger = messenger;}@Overridepublic PlatformView create(Context context, int id, Object o) {return new WubarnView(context, messenger, id);}}
WubarnView的具体实现
public class WubarnView implements PlatformView, MethodChannel.MethodCallHandler{private final ReactRootView mReactRootView;private final ReactInstanceManager mReactInstanceManager;public WubarnView(Context context, BinaryMessenger messenger, int id) {MethodChannel methodChannel = new MethodChannel(messenger, WubarnPlugin.VIEW_TYPE + "_" + id);methodChannel.setMethodCallHandler(this);// ReactNative的创建及初始化,设置其默认加载的bundle名称mReactRootView = new ReactRootView(context);mReactInstanceManager = ReactInstanceManager.builder().setApplication((Application) context.getApplicationContext()).setBundleAssetName("index.android.bundle").setJSMainModulePath("index").addPackage(new MainReactPackage()).setUseDeveloperSupport(false).setInitialLifecycleState(LifecycleState.RESUMED).build();// 这个"App1"名字一定要和我们在index.js中注册的名字保持一致AppRegistry.registerComponent()mReactRootView.startReactApplication(mReactInstanceManager, "App1", null);}@Overridepublic View getView() {return mReactRootView;}@Overridepublic void dispose() {// mReactInstanceManager.onHostPause(mActivity);// mReactInstanceManager.onHostResume(mActivity, null);// mReactInstanceManager.onHostDestroy(mActivity);mReactRootView.unmountReactApplication();}@Overridepublic void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {switch (methodCall.method){case "":break;default:result.notImplemented();}}}
上面初始化ReactInstanceManager当中的常量,与React代码是一一对应的
“App1”:与在React里注册的组件名称是一样的
import { AppRegistry } from 'react-native';import App from './App';AppRegistry.registerComponent('App1', () => App);
.setJSMainModulePath(“index”):JS bundle中主入口的文件名,是React工程里的入口文件index.js的名称
.setBundleAssetName(“index.android.bundle”):这个是内置到assets目录下的bundle名称,与bundle生成命令有关
react-native bundle --platform android --dev false --entry-file index.js --bundle-output /Users/ly/liuyang/workspace_flutter/wubarn_plugin/example/android/app/src/main/assets/index.android.bundle --assets-dest /Users/ly/liuyang/workspace_flutter/wubarn_plugin/example/android/app/src/main/res/
实现效果
实现思路:
通过PageRoute,去掉切换的动画
通过AnimatedBuilder,实现旋转动画
通过WillPopScope Widget拦截返回事件
Flutter的页面切换是由Navigator管理,其中有一个栈,栈帧是路由,通过PageRoute可以自定义切换的动画,如下去掉切换动画的代码:
Navigator.push(context, PageRouteBuilder(transitionDuration: Duration(), // 去掉了执行动画的时间pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation){return PublishHome();}));
由于Flutter是MVVM框架,Flutter里的Animation只负责计算,不负责界面布局与渲染,需要手动调用setState()来让界面重绘,不过可以通过AnimatedBuilder简化流程,但Flutter在实现组合动画比较麻烦。
class PublishHome extends StatefulWidget {@override_PublishHomeState createState() => _PublishHomeState();}class _PublishHomeState extends State<PublishHome> with SingleTickerProviderStateMixin {Animation<double> animation;AnimationController controller;@overridevoid initState() {super.initState();controller = AnimationController(duration: Duration(milliseconds: 200), vsync: this);animation = Tween(begin: 0.0, end: 45.0).animate(controller);animation.addStatusListener((AnimationStatus status){if(status == AnimationStatus.dismissed) {Navigator.pop(context);}});controller.forward();}@overrideWidget build(BuildContext context) {return WillPopScope(onWillPop: () async {controller.reverse();return false;},child: Scaffold(backgroundColor: Colors.white,body: SafeArea(top: true,child: Stack(children: <Widget>[...Positioned(left: 0,right: 0,bottom: 0,height: 63,child: GestureDetector(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[AnimatedBuilder(animation: this.animation,builder: (BuildContext context, Widget child){return Transform.rotate(angle: animation.value * math.pi / 180.0,child: child,);},child: Image.asset('assets/images/home/wb_home_tab_publish_img.png',width: 40,height: 40,fit: BoxFit.contain),),Text('发布',style: TextStyle(color: Colors.white),)],),onTap: (){controller.reverse();},),)],),),));}}
要求:
不使用图片实现
背景支持渐变
不要点击效果
Material Widget里的四种Button无法满足按钮要求,第三方渐变按钮也无法完全满足要求,通过Container Widget的decoration自定义此Widget:
import 'package:flutter/material.dart';class GradientButton extends StatelessWidget {final double width;final double height;final Gradient gradient;final Widget child;final Function onTap;final BorderRadius shapeRadius;const GradientButton({Key key, this.width, this.height, this.gradient, this.onTap, this.shapeRadius, this.child}): super(key: key);@overrideWidget build(BuildContext context) {return GestureDetector(onTap: this.onTap,child: Container(width: this.width,height: this.height,decoration: BoxDecoration(gradient: this.gradient, // 设置渐变borderRadius: this.shapeRadius // 设置圆角),child: Center(child: child,)),);}}
实现效果
要求:
BottomSheet增加中间态
有回弹效果
第三方库RubberBottomSheet实现了此效果,其原理如下:
通过Stack实现叠加布局
修改AnimationController的原码,依据lowerBound,upperBound的实现思路,实现halfBound,即中间态
直接使用RubberBottomSheet的代码非常简单:
class TribePublish extends StatefulWidget {@override_TribePublishState createState() => _TribePublishState();}class _TribePublishState extends State<TribePublish> with SingleTickerProviderStateMixin {RubberAnimationController _controller;@overridevoid initState() {super.initState();_controller = RubberAnimationController(vsync: this,lowerBoundValue: AnimationControllerValue(pixel: 54),halfBoundValue: AnimationControllerValue(pixel: 300),upperBoundValue: AnimationControllerValue(percentage: 1.0),duration: Duration(milliseconds: 200));}@overrideWidget build(BuildContext context) {return Scaffold(backgroundColor: Colors.white,appBar: AppBar(title: Text('部落发布'),),body: RubberBottomSheet(header: _getHeader(),lowerLayer: _getLowerLayer(),upperLayer: _getUpperLayer(),animationController: _controller,));}}
加载相册图片
通过MethodChannel,实现与Native通信,加载相册图片
在Android里,加载相册图片,需要先授权
防止相册图片过多,需进行分页加载
Android端的代码实现:
public class AlbumManagerPlugin implements MethodChannel.MethodCallHandler {public static void registerWith(PluginRegistry registry) {registerWith(registry.registrarFor("com.wuba.plugins.AlbumManagerPlugin"));}public static void registerWith(PluginRegistry.Registrar registrar){final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.wuba.com/album_manager");channel.setMethodCallHandler(new AlbumManagerPlugin(registrar.context(), registrar));}/** * the page size of query albums */public static final int PAGE_SIZE = 200;private final Context mContext;private final PluginRegistry.Registrar mRegistrar;private PermissionsUtils mPermissionsUtils;public AlbumManagerPlugin(Context context, PluginRegistry.Registrar registrar) {this.mContext = context;mRegistrar = registrar;mPermissionsUtils = new PermissionsUtils();registrar.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() {@Overridepublic boolean onRequestPermissionsResult(int i, String[] strings, int[] ints) {mPermissionsUtils.dealResult(i, strings, ints);return false;}});}@Overridepublic void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {// 先申请权限mPermissionsUtils.setPermissionsListener(new PermissionsListener() {@Overridepublic void onDenied(String[] deniedPermissions) {Log.i("permission", "onDenied call.method = ${call.method}");result.error("失败", "权限被拒绝", "");}@Overridepublic void onGranted() {switch (methodCall.method){case "getAllImage":getAllImage(methodCall, result);break;default:result.notImplemented();}}});mPermissionsUtils.withActivity(mRegistrar.activity());mPermissionsUtils.getPermissions(mRegistrar.activity(), 3001, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);}private void getAllImage(MethodCall methodCall, MethodChannel.Result result) {List<String> list = new ArrayList<String>();// int pageIndex = methodCall.argument("pageIndex");int pageIndex = 0;Log.d("liuyang", "" + methodCall.argument("pageIndex"));String[] projection = {MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME};String sortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC limit " + PAGE_SIZE + " offset " + pageIndex * PAGE_SIZE;//执行分页String selection = null;// if (!ALL_PHOTO.equals(s)) {// selection = MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME + " = '" + s + "' ";// }Cursor cursor = mContext.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,projection, selection, null, sortOrder);try {if (cursor != null) {while (cursor.moveToNext()) {// 获取图片的路径String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));list.add(path);}result.success(list);}} catch (Exception e) {// LOGGER.e(TAG, e.toString());result.error("AlbumManagerPlugin", e.getMessage(), "");} finally {if (cursor != null) {cursor.close();}}}}
Flutter端的代码实现:
class AlbumManagerPlugin {static const MethodChannel _channel = MethodChannel('plugins.wuba.com/album_manager');static Future<List<AssetEntity>> getAllAssetList(int pageIndex) async {Map<dynamic, dynamic> map = Map<dynamic, dynamic>();map['pageIndex'] = pageIndex;List<dynamic> paths = await _channel.invokeMethod('getAllImage', map);return _castAsset(paths);}static Future<List<AssetEntity>> _castAsset(List<dynamic> paths) async {List<AssetEntity> result = <AssetEntity>[];for (var i = 0; i < paths.length; i++) {result.add(AssetEntity(path: paths[i]));}return result;}}
细节点:
Native的扩展能力定义为Plugin,Plugin可以独立发布为一个库,里面即有native代码也有dart代码,不用像ReactNative,需要单独合并native的代码,但带的问题是:dependencies库都是直接原码
通过MethodChannel进行Flutter与Native通信,可以传递参数,如何传递一组参数了,通过源码分析:Map对象
分页显示图片
通过GridView显示图片,实现分页加载
默认的图片加载策略是LRU,体验与内存表现都很不好
下面的代码没有实现分页与图片加载策略的优化:
class AlbumGrid extends StatefulWidget {@override_AlbumGridState createState() => _AlbumGridState();}class _AlbumGridState extends State<AlbumGrid> {List<AssetEntity> list = new List<AssetEntity>();int currentPage = -1;@overridevoid initState() {super.initState();// 加载第一页数据_initData(0);}void _initData(int nextPage) async {List<AssetEntity> newPage = await AlbumManagerPlugin.getAllAssetList(nextPage);this.setState((){list.addAll(newPage);currentPage = nextPage;});}@overrideWidget build(BuildContext context) {return GridView.builder(gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,mainAxisSpacing: 4.0,crossAxisSpacing: 4.0),padding: EdgeInsets.all(4.0),itemBuilder: _itemBuilder,itemCount: list.length,);}Widget _itemBuilder(BuildContext context, int index) {AssetEntity entity = list[index];return Image.file(File(entity.path),fit: BoxFit.cover,);}}
Flutter框架在设计上,整体优于其他跨平台框架,实现使用时,也是非常的方便,有如下感受:
开发调试非常的快,比Android的instant run强很多,也稳定很多
dependencies依赖管理比ReactNative强,native扩展能力是一个独立的plugin库,便于管理依赖
基于MVVM框架,在自定义UI组件及动画方面,结构清楚,容易理解
实现相同的功能,代码量远小于使用java实现
由于Flutter的社区不太完善,时间太短,生态不完善,相当于2011年开发Android一样,缺少大量成熟的基础库,大量的基础能力都需要从头到尾开发,下面是上述实践过程中发现的一些点:
渐变Button,图片Button
GridView或ListView的图片加载策略(Fling时不加载,scrolling或idle时加载)
崩溃日志收集
大量的基础Plugin:加载相册,授权,地图,视频等等
…
在已经集成ReactNative的58App里,已经基本满足部分业务的动态能力,再花大量的成本完美Flutter的基础,花大量的成本去推动业务线使用,短期来看,投入产出比太低。
但从长期来看,在跨平台框架上,我更加看好Flutter,在设计与使用体验上,Flutter确实都优于其他框架,但Flutter最终能否成为主流,还是要看Google的推广力度。
持续关注跨平台框架的动态,ReactNative也在向Flutter学习,改进其性能差的一面,Flutter的基础库也在不断的完善中
此demo的代码:wuba_gallery
React Native 混合开发(Android篇)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。