这篇文章将概述 Android组件化的架构搭建 及 Flutter 和 Android 如何混合开发 (整个App只有首页是用原生Android完成,其他页面都是引入之前的做好的Flutter页面) ,主宿主程序由 Android 搭建,采用了组件化的架构搭建整个 App ,不同业务,对应不同的 module 工程,业务之间采用接口通信 (ARouter),以 module 的形式混入 Flutter,通过 MethodChannel 和 Flutter 端进行数据通信等,且这些功能实现源码开源,感兴趣的小伙伴可以移步至 GitHub。
以下博文会分为4个部分概述:
其次,分析梳理下项目结构,项目的结构大致如图,还有一些细枝末节的没有体现在图里:
业务工程
把具体独立的业务都拆分成单独的 module 减小项目的维护压力
把具体的功能都封装成独立的库供业务模块使用,降低项目的维护成本及代码之间耦合性
这里有一些使用的插件并没有在项目结构图里体现出来(结构图空间有限)。
在这里把项目使用的插件整理列举出来供大家参考:
findViewById
的代码。基本就是这些了,应该没有漏的,插件的详细使用,请进入各插件的 GitHub 主页。
在此,把我项目的插件引入代码及版本管理的 gradle
代码贴出来,如下:
插件引入代码:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation rootProject.depsLibs.appcompat
implementation rootProject.depsLibs.legacy
implementation rootProject.depsLibs.recyclerview
implementation rootProject.depsLibs.constraintlayout
implementation rootProject.depsLibs.cardview
//tab指示器
implementation rootProject.depsLibs.magicindicator
//沉浸式
implementation rootProject.depsLibs.immersionbar
//导航栏
implementation rootProject.depsLibs.pagerBottomTabStrip
//rxjava
implementation rootProject.depsLibs.rxjava
//rxandroid
implementation rootProject.depsLibs.rxandroid
//view 注入
implementation rootProject.depsLibs.butterknife
//view 注入
annotationProcessor rootProject.depsLibs.butterknifeCompiler
//gson
implementation rootProject.depsLibs.gson
//banner
implementation rootProject.depsLibs.banner
//smartRefreshLayout 上下拉刷新
implementation rootProject.depsLibs.smartRefreshLayout
implementation rootProject.depsLibs.refreshHeader
implementation rootProject.depsLibs.refreshHeaderTwoLevel
implementation rootProject.depsLibs.refreshFooter
//eventbus
implementation rootProject.depsLibs.eventbus
//arouter库
implementation(rootProject.depsLibs.arouterapi) {
exclude group: 'com.android.support'
}
annotationProcessor rootProject.depsLibs.aroutercompiler
//引入home模块
implementation project(':ft_home')
//引入图片加载库
implementation project(':lib_image_loader')
//引入网络库
implementation project(':lib_network')
//webview
implementation project(':lib_webview')
//引入基础ui库
implementation project(':lib_common_ui')
//base库
implementation project(':lib_base')
//引入flutter模块
implementation project(':flutter')
//引入百度AI语音库
implementation project(':lib_asr')
}
版本管理代码 (统一管理版本号) :
ext {
android = [
compileSdkVersion: 29,
buildToolsVersion: "29.0.0",
minSdkVersion : 19,
targetSdkVersion : 29,
applicationId : 'net.lishaoy.android_ctrip',
versionCode : 1,
versionName : '1.0',
multiDexEnabled : true,
]
depsVersion = [
appcompat : '1.1.0',
legacy : '1.0.0',
recyclerview : '1.0.0',
constraintlayout : '1.1.3',
cardview : '1.0.0',
magicindicator : '1.5.0',
immersionbar : '3.0.0',
pagerBottomTabStrip : '2.3.0X',
glide : '4.11.0',
glidecompiler : '4.11.0',
butterknife : '10.2.1',
butterknifeCompiler : '10.2.1',
rxjava : '3.0.0',
rxandroid : '3.0.0',
okhttp : '4.7.2',
okhttpLogging : '4.7.2',
gson : '2.8.6',
banner : '2.0.10',
smartRefreshLayout : '2.0.1',
refreshHeader : '2.0.1',
refreshFooter : '2.0.1',
refreshHeaderTwoLevel: '2.0.1',
eventbus : '3.2.0',
agentweb : '4.1.3',
arouterapi : '1.5.0',
aroutercompiler : '1.2.2',
]
depsLibs = [
appcompat : "androidx.appcompat:appcompat:${depsVersion.appcompat}",
legacy : "androidx.legacy:legacy-support-v4:${depsVersion.legacy}",
recyclerview : "androidx.recyclerview:recyclerview:${depsVersion.recyclerview}",
constraintlayout : "androidx.constraintlayout:constraintlayout:${depsVersion.constraintlayout}",
cardview : "androidx.cardview:cardview:${depsVersion.cardview}",
magicindicator : "com.github.hackware1993:MagicIndicator:${depsVersion.magicindicator}",
immersionbar : "com.gyf.immersionbar:immersionbar:${depsVersion.immersionbar}",
pagerBottomTabStrip : "me.majiajie:pager-bottom-tab-strip:${depsVersion.pagerBottomTabStrip}",
glide : "com.github.bumptech.glide:glide:${depsVersion.glide}",
glidecompiler : "com.github.bumptech.glide:compiler:${depsVersion.glidecompiler}",
butterknife : "com.jakewharton:butterknife:${depsVersion.butterknife}",
butterknifeCompiler : "com.jakewharton:butterknife-compiler:${depsVersion.butterknifeCompiler}",
rxjava : "io.reactivex.rxjava3:rxjava:${depsVersion.rxjava}",
rxandroid : "io.reactivex.rxjava3:rxandroid:${depsVersion.rxandroid}",
okhttp : "com.squareup.okhttp3:okhttp:${depsVersion.okhttp}",
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:${depsVersion.okhttpLogging}",
gson : "com.google.code.gson:gson:${depsVersion.gson}",
banner : "com.youth.banner:banner:${depsVersion.banner}",
smartRefreshLayout : "com.scwang.smart:refresh-layout-kernel:${depsVersion.smartRefreshLayout}",
refreshHeader : "com.scwang.smart:refresh-header-classics:${depsVersion.refreshHeader}",
refreshHeaderTwoLevel: "com.scwang.smart:refresh-header-two-level:${depsVersion.refreshHeader}",
refreshFooter : "com.scwang.smart:refresh-footer-classics:${depsVersion.refreshFooter}",
eventbus : "org.greenrobot:eventbus:${depsVersion.eventbus}",
agentweb : "com.just.agentweb:agentweb:${depsVersion.agentweb}",
arouterapi : "com.alibaba:arouter-api:${depsVersion.arouterapi}",
aroutercompiler : "com.alibaba:arouter-compiler:${depsVersion.aroutercompiler}",
]
}
首页重点概述以下功能的实现:
首先,看看具体的效果图,如图:
下拉刷新和携程二楼是使用 smartRefreshLayout 插件完成的,实现代码如下:
private void initRefreshMore() {
homeHeader.setRefreshHeader(new ClassicsHeader(getContext()), -1, (int) Utils.dp2px(76)); //设置下拉刷新及二楼header的高度
homeHeader.setFloorRate(1.6f); //设置二楼触发比率
homeRefreshContainer.setPrimaryColorsId(R.color.colorPrimary, R.color.white); //设置下拉刷新及二楼提示文字颜色
homeRefreshContainer.setOnMultiListener(new SimpleMultiListener() {
@Override
public void onLoadMore(@NonNull RefreshLayout refreshLayout) {
loadMore(refreshLayout); //加载更多
}
@Override
public void onRefresh(@NonNull RefreshLayout refreshLayout) {
refreshLayout.finishRefresh(1600); //设置下拉刷新延迟
}
@Override
public void onHeaderMoving(RefreshHeader header, boolean isDragging, float percent, int offset, int headerHeight, int maxDragHeight) {
homeSecondFloorImg.setVisibility(View.VISIBLE); //隐藏二楼背景图
homeSearchBarContainer.setAlpha(1 - Math.min(percent, 1)); //改变searchBar透明度
}
@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
if (oldState == RefreshState.ReleaseToTwoLevel) { //即将去往二楼状态处理
homeSecondFloorImg.setVisibility(View.GONE);
homeHeaderContent.animate().alpha(1).setDuration(666);
} else if (newState == RefreshState.PullDownCanceled) { //下拉取消状态处理
homeHeaderContent.animate().alpha(0).setDuration(666);
} else if (newState == RefreshState.Refreshing) { //正在刷新状态处理
homeHeaderContent.animate().alpha(0).setDuration(666);
} else if (oldState == RefreshState.TwoLevelReleased) { // 准备去往二楼完成状态处理,这里打开webview
WebViewImpl.getInstance().gotoWebView("https://m.ctrip.com/webapp/you/tsnap/secondFloorIndex.html?isHideNavBar=YES&s_guid=feb780be-c55a-4f92-a6cd-2d81e04d3241", true);
homeHeader.finishTwoLevel();
} else if (oldState == RefreshState.TwoLevel) { //到达二楼状态处理
homeCustomScrollView.setVisibility(View.GONE);
homeHeaderContent.animate().alpha(0).setDuration(666);
} else if (oldState == RefreshState.TwoLevelFinish) { //二楼完成状态处理
homeCustomScrollView.setVisibility(View.VISIBLE);
homeCustomScrollView.animate().alpha(1).setDuration(666);
}
}
});
}
XML
页面布局文件代码如下:
...
具体实现详情,可移步 GitHub 查看源码。
搜索栏的滚动的 placeholder 文字是使用 banner 插件实现的,点击搜索框可跳转到搜索页面 (flutter写的搜索页面) ,跳转页面后可以把 placeholder 文字带到 flutter 搜索页面。
效果如图:
滚动的placeholder文字实现代码如下 (搜索框的实现就不再这里展示都是一些XML布局代码):
homeSearchBarPlaceholder
.setAdapter(new HomeSearchBarPlaceHolderAdapter(homeData.getSearchPlaceHolderList())) // 设置适配器
.setOrientation(Banner.VERTICAL) // 设置滚动方向
.setDelayTime(3600) // 设置间隔时间
.setOnBannerListener(new OnBannerListener() {
@Override
public void OnBannerClick(Object data, int position) { //点击打开 flutter 搜索页面
ARouter.getInstance()
.build("/home/search")
.withString("placeHolder", ((Home.SearchPlaceHolderListBean) data).getText())
.navigation();
}
});
}
searchBar的具体功能不过多阐述,和之前的项目一致。
渐变色网格导航基本都是一些 XML
页面布局代码,只是我把它封装成了单独的组件,效果如图
封装之后的引入就非常简单,代码如下:
...
具体实现详情,可移步 GitHub 查看源码。
banner组件也是用 banner 插件实现的,如图
实现代码如下:
private void initBanner() {
homeBanner.addBannerLifecycleObserver(this)
.setAdapter(new HomeBannerAdapter(homeData.getBannerList())) //设置适配器
.setIndicator(new EllipseIndicator(getContext())) //设置指示器,如图的指示器是我自定义的插件里并没有提供
.setIndicatorSelectedColorRes(R.color.white) //设置指示器颜色
.setIndicatorSpace((int) BannerUtils.dp2px(10)) //设置间距
.setBannerRound(BannerUtils.dp2px(6)); //设置圆角
}
多状态的tab指示器的实现需要注意很多细节,因为它是在首页的 fragment
的 ScrollView
里嵌入 viewPaper
,首先你会发现 viewPaper 不显示的问题,其次是滚动不流畅的问题,这两个问题我的解决方案是:
ViewPager
重写 onMeasure
方法,重新计算高度ScrollView
,重写 computeScroll
和 onScrollChanged
重新获取滚动距离实现效果如图:
这个功能实现代码过多不便在这里展示,具体实现详情,可移步 GitHub 查看源码。
这个项目的实现只有首页是用 Android 原生实现,其他的页面均是 Flutter 实现的,之前 纯Flutter项目。
Android 引入 Flutter 进行混合开发,需要以下几个步骤
下面依次概述这几部分是如何操作实现的。
这个应该不用过多描述,基本操作大家都会 File --> New --> New Module 如图:
新建完成之后,android studio 会自动生成配置代码到 gradle 配置文件里,且生成一个 flutter 的 library 模块。
Tips:
新建的时候最好 flutter module 和 android 项目放到同级目录下;
新版的 android studio 才会自动生成 gradle 配置代码,老版本貌似需要手动配置
如,没有生成 gradle 配置代码,你需要在根项目的 settings.gradle
文件里手动加入如下配置:
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir, //设置根路径,根据具体flutter module路径配置
'flutter_module/.android/include_flutter.groovy'
))
include ':flutter_module'
还需在宿主工程 (没改名的话都是app) 的 build.gradle
引入 flutter, 如下:
dependencies {
...
//引入flutter模块
implementation project(':flutter')
...
}
编写flutter代码,在 flutter module 里按照正常 flutter 开发流程编写 flutter 代码即可。 (我项目里的 flutter 的代码是之前项目都写好的,复制过来,改改包的引入问题,就可以运行了。)
这里需要注意的是,flutter 有且只有一个入口,就是 main()
函数,我们需要在这里处理好 flutter 页面的跳转问题。
在 android 端,创建 flutter 页面,代码如下:
Flutter.createView(getActivity(),getLifecycle(),"destination");
Flutter.createView
需要3个参数 activity
、lifecycle
、route
,这个 route 就是要传递到 flutter 端的,当然,它是 String 类型的,我们可以自由发挥传递普通字符串或 json 字符串等。
我们也可以通过其他的方式创建 flutter 页面,如: Flutter.createFragment()
、 FlutterActivity.withNewEngine()
、 FlutterFragment.createDefault()
等。
具体的使用,可前往 Flutter官方文档 查阅。
那么,flutter 端如何接收这个 route 参数,是通过 window.defaultRouteName
,此项目里管理 flutter 端路由代码如下:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter model',
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'PingFang',
),
home: _widgetRoute(window.defaultRouteName), // 通过 window.defaultRouteName 接收 android 端传来的参数
);
}
}
Widget _widgetRoute(String defaultRouteName) {
Map params = convert.jsonDecode(defaultRouteName); //解析参数
defaultRouteName = params['routeName'];
placeHolder = params['placeHolder'];
switch (defaultRouteName) { // 根据参数返回对应的页面
...
case 'destination/search':
return DestinationSearchPage(
hideLeft: false,
);
...
default:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('not found $defaultRouteName',
textDirection: TextDirection.ltr),
],
),
);
}
}
其实,flutter 端接收这个 route 参数,还有一种方法,就是通过 onGenerateRoute
,它是 MaterialApp 里的一个方法。
代码如下:
onGenerateRoute: (settings){ //通过 settings.name 获取android端传来的参数
return _widgetRoute(settings.name);
},
flutter 端可以调用 android 端的方法及相互传递数据是如何实现的,flutter 官方提供了3个方法可以实现,分别是:
此项目里采用了 MethodChannel
方法进行通信,如:flutter 端调用 android 端的AI智能语音方法以及 flutter 打开 android 端页面就是用 MethodChannel
实现的。
flutter 端调用 android 端的AI智能语音方法代码如下:
class AsrManager {
static const MethodChannel _channel = const MethodChannel('lib_asr');
//开始录音
static Future start({Map params}) async {
return await _channel.invokeMethod('start', params ?? {});
}
//停止录音
...
//取消录音
...
//销毁
...
}
flutter 打开 android 端页面代码如下:
class MethodChannelPlugin {
static const MethodChannel methodChannel = MethodChannel('MethodChannelPlugin');
static Future gotoDestinationSearchPage() async {
try {
await methodChannel.invokeMethod('gotoDestinationSearchPage'); //gotoDestinationSearchPage 参数会传到android端
} on PlatformException {
print('Failed go to gotoDestinationSearchPage');
}
}
...
}
android 接收也是通过 MethodChannel
,具体实现代码如下:
public class MethodChannelPlugin implements MethodChannel.MethodCallHandler {
private static MethodChannel methodChannel;
private Activity activity;
private MethodChannelPlugin(Activity activity) {
this.activity = activity;
}
//调用方通过 registerWith 来注册flutter页面
public static void registerWith(FlutterView flutterView) {
methodChannel = new MethodChannel(flutterView, "MethodChannelPlugin");
MethodChannelPlugin instance = new MethodChannelPlugin((Activity) flutterView.getContext());
methodChannel.setMethodCallHandler(instance);
}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if (methodCall.method.equals("gotoDestinationSearchPage")) { // 收到消息进行具体操作
EventBus.getDefault().post(new GotoDestinationSearchPageEvent());
result.success(200);
}
...
else {
result.notImplemented();
}
}
}