自上篇 Flutter 10天高仿大廠App及小技巧積累總結 的續篇,此次更是乾貨滿滿。html
這篇文章將概述 Android組件化的架構搭建 及 Flutter 和 Android 如何混合開發 (整個App只有首頁是用原生Android完成,其餘頁面都是引入以前的作好的Flutter頁面) ,主宿主程序由 Android 搭建,採用了組件化的架構搭建整個 App ,不一樣業務,對應不一樣的 module 工程,業務之間採用接口通訊 (ARouter) ,以 module 的形式混入 Flutter,經過 MethodChannel 和 Flutter 端進行數據通訊等,且這些功能實現源碼開源,感興趣的小夥伴能夠移步至 GitHub。java
如下博文會分爲4個部分概述:react
首先,咱們仍是經過一個視頻來快速預覽下項目完成的功能和運行效果,以下android
你們也能夠 點擊觀看視頻 (點擊齒輪 --> 更多播放設置,能夠隱藏黑邊)git
看完視頻後,其實大部分功能和以前的 純flutter項目 功能相同,只是首頁新增了4個tab推薦頁面及攜程二樓和佈局改變。github
你們也可掃描,安裝體驗:web
其次,分析梳理下項目結構,項目的結構大體如圖,還有一些細枝末節的沒有體如今圖裏:編程
把具體獨立的業務都拆分紅單獨的 module 減少項目的維護壓力json
把具體的功能都封裝成獨立的庫供業務模塊使用,下降項目的維護成本及代碼之間耦合性api
這裏有一些使用的插件並無在項目結構圖裏體現出來(結構圖空間有限)。
在這裏把項目使用的插件整理列舉出來供你們參考:
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}", ] }
這裏主要對首頁功能及知識點進行概述,因爲其餘頁面是引入了以前的 Flutter 頁面, 具體功能在 Flutter 10天高仿大廠App及小技巧積累總結 已經介紹過了,在這就再也不闡述。
首頁重點概述如下功能的實現:
首先,看看具體的效果圖,如圖:
下拉刷新和攜程二樓是使用 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
頁面佈局文件代碼以下:
<com.scwang.smart.refresh.layout.SmartRefreshLayout android:id="@+id/home_refresh_container" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" app:srlAccentColor="@color/colorPrimary" app:srlPrimaryColor="@color/colorPrimary"> <com.scwang.smart.refresh.header.TwoLevelHeader android:id="@+id/home_header" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="top"> <ImageView android:id="@+id/home_second_floor_img" android:layout_width="match_parent" android:layout_height="460dp" android:layout_alignTop="@+id/home_header" android:scaleType="fitXY" android:src="@drawable/second_floor" android:visibility="gone"/> <FrameLayout android:id="@+id/home_header_content" android:layout_width="match_parent" android:layout_height="match_parent" android:alpha="0"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY" android:src="@drawable/second_floor" /> </FrameLayout> </com.scwang.smart.refresh.header.TwoLevelHeader> ... <com.scwang.smart.refresh.footer.ClassicsFooter android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.scwang.smart.refresh.layout.SmartRefreshLayout>
具體實現詳情,可移步 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
頁面佈局代碼,只是我把它封裝成了單獨的組件,效果如圖
封裝以後的引入就很是簡單,代碼以下:
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@color/white"> <!-- 網格導航 --> <net.lishaoy.ft_home.GridNavView android:id="@+id/home_grid_nav_container" android:layout_width="match_parent" android:layout_height="wrap_content" /> ... </LinearLayout>
具體實現詳情,可移步 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<String, dynamic> 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: <Widget>[ 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<String> start({Map params}) async { return await _channel.invokeMethod('start', params ?? {}); } //中止錄音 ... //取消錄音 ... //銷燬 ... }
flutter 打開 android 端頁面代碼以下:
class MethodChannelPlugin { static const MethodChannel methodChannel = MethodChannel('MethodChannelPlugin'); static Future<void> 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(); } } }
android flutter 混合開發基本就是這3個步驟,其餘一些細節及具體的流程請參考 GitHub 項目源碼。
最後附上項目地址和博客地址:
項目地址:https://github.com/persilee/android_ctrip
博客地址:https://h.lishaoy.net/androidctrip