原生工程接入Flutter實現混編

来源:https://www.cnblogs.com/cnchemmy/archive/2020/07/01/13220213.html
-Advertisement-
Play Games

前言 上半年我定的OKR目標是幫助團隊將App切入Flutter,實現統一技術棧,變革成多端融合開發模式。Flutter目前是跨平臺方案中最有潛力實現我們這個目標的,不管是Hybird還是React Native,我們的項目都有落地應用,跨平臺一直是終端團隊所追求的技術,能夠快速研發和部署也是我們不 ...


前言

上半年我定的OKR目標是幫助團隊將App切入Flutter,實現統一技術棧,變革成多端融合開發模式。Flutter目前是跨平臺方案中最有潛力實現我們這個目標的,不管是Hybird還是React Native,我們的項目都有落地應用,跨平臺一直是終端團隊所追求的技術,能夠快速研發和部署也是我們不斷給自己提出的挑戰。Flutter是什麼我在這裡就不多說了,很多文章都有介紹,本篇文章想分享的是如何在原生工程中嵌入Flutter來實現混編,幫助團隊快速落地Flutter遷移,這個對小團隊來說應該會有一定借鑒意義。

前置動作

在接入Flutter之前需要具備以下前置條件:

  1. 易於開發的操作系統(首推macOS)
  2. 配置Flutter開發環境(參考:https://flutter.dev/docs/get-started/install/macos )
  3. Android和iOS開發環境(自行搜索解決)

接入方案

業內絕大部分的App都不可能推倒重來,所以混合工程的方式接入Flutter是目前主流開發模式,下麵我簡單說說業界兩種工程管理模式:

在這裡插入圖片描述
統一管理模式(不推薦)

  • 優點
    • 適合全新使用Flutter開發的項目
  • 缺點
    • 後期代碼耦合嚴重,相關工具鏈耗時大幅增長,導致開發效率低

三端分離模式(推薦)

鹹魚方案:https://mp.weixin.qq.com/s/Q1z6Mal2pZbequxk5I5UYA?​
官方方案:https://flutter.dev/docs/development/add-to-app​

  • 優點
    • 快速實現Flutter功能“熱插拔”,降低原生工程的改造成本
    • 可以直接進行Dart代碼和原生代碼開發調試

目前我們採用的是以module的形式接入,因為我們團隊人員少,溝通協作起來成本不大,初期直接源碼接入也方便我們快速接入開發和調試。

踩坑實踐

flutter doctor
flutter doctor
如果想確認你當前的環境是否ok,執行下flutter doctor命令,基本能解決大部分問題。如果遇到一直卡住,說明你當前環境是不通的,檢查下代理是否配置正確。

創建Flutter module工程
創建Flutter module工程
如果點擊Finish創建module一直卡死,說明還是網路問題,命令行輸入vi ~/.bash_profile檢查下代理。如果實在不行,則通過命令行創建module:

 flutter create -t module --org com.example my_flutter

Android原生工程集成Flutter

一期我們先接入Android工程,所以接下來主要以Android為主,後續如果有iOS相關的實踐會補充到這裡。

先看下我們的module工程:
在這裡插入圖片描述
目錄結構:
在這裡插入圖片描述

  • .android(隱藏目錄,自動生成的Android工程)
  • .ios(隱藏目錄,自動生成的iOS工程)
  • build(Android和iOS的構建產物)
  • lib(Flutter應用源文件)
  • test(測試文件)
  • *.iml(工程配置文件)
  • pubspec.lock(記錄當前項目實際依賴信息的文件)
  • pubspec.yaml(管理第三方庫資源信息的配置文件)

除了工程配置文件和自動生成的工程目錄之外,其他文件都需要進行托管。

瞭解完工程目錄之後,我們開始集成:

  1. 打開原生工程setting.gradle,加入以下配置
// 引入flutter module
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
        settingsDir.parentFile,                                               // new
        'edu_flutter_module/.android/include_flutter.groovy'                          // new
))



include ':edu_flutter_module'
project(':edu_flutter_module').projectDir = new File('../edu_flutter_module')

可以看到目前我們依賴的flutter module,是在原生工程目錄同級的。

  1. 打開主工程的build.gradle文件,在dependencies下加入以下配置:
    implementation project(":flutter")

ok,這兩步是官方的指引,配置完之後就完事了? 太天真了,還需要有一些額外的調整。構建一下就知道了:

異常1:Gradle DSL method not found: 'google()'

在這裡插入圖片描述
項目中用的gradle版本還是比較舊的,需要升級一下:
在這裡插入圖片描述

異常2:AAPT error:resource android:attr/fontVariationSettings not found
在這裡插入圖片描述
這個異常需要將compileSdkVersion升級到28,之前是26。

異常3:assert appProject !=null
在這裡插入圖片描述
這個問題巨坑,我們的主工程名是course,但flutter的構建腳本是硬編碼為app,有兩種解決辦法:

  1. 重命名module名字,命名為app
  2. 修改flutter腳本(我選的是這種)

在這裡插入圖片描述
這樣,flutter腳本就能找到我們的工程,編譯也ok了。

但其實還有問題,因為目前我們還未升級support包到AndroidX版本,而創建出來的module工程預設是支持AndroidX的,所以我們需要進行降級,等後續升級工程之後再處理。

修改edu_flutter_module/pubspec.yaml,將androidX改為false:

  module:
    androidX: false
    androidPackage: com.tencent.edu
    iosBundleIdentifier: com.tencent.edu

改完這個之後,終於工程編譯通過了,但這就結束了嗎,還有坑等著你呢。

原生頁面引入Flutter頁面

上一個主題我們解決掉一些坑之後終於把flutter作為一個module集成到我們的工程中,接下來我們嘗試寫個頁面嵌入到我們頁面。

目前課堂用的flutter版本是:v1.12.13+hotfix.5,這個版本的使用跟之前的版本會有些差異,可以參考官方的wiki:

https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects

這裡我嘗試把課堂的首頁替換成Flutter頁面,做了以下調整:


 @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // TODO: 2020-04-01 增加flutter視圖
        View view = inflater.inflate(R.layout.fragment_index, container, false);

        FlutterEngine flutterEngine = new FlutterEngine(getActivity());
        flutterEngine.getDartExecutor().executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
        );
        flutterEngine.getNavigationChannel().setInitialRoute("route1");


        FlutterView flutterView = new FlutterView(getActivity());
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        FrameLayout flContainer = view.findViewById(R.id.fl_content);
        // 關鍵代碼,將Flutter頁面顯示到FlutterView
        flutterView.attachToFlutterEngine(flutterEngine);
        flContainer.addView(flutterView, lp);
        return view;
    }

fragment_index.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 嵌入flutter視圖    -->
    <FrameLayout
        android:id="@+id/fl_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</LinearLayout>

dart代碼實現:

main.dart

import 'package:edu/home_page.dart';
import 'package:flutter/material.dart';
import 'dart:ui';
import 'dart:convert';

void main() {
  runApp(_widgetForRoute(window.defaultRouteName));
}
// 獲取路由名稱
String _getRouteName(String s) {
  if (s.indexOf('?') == -1) {
    return s;
  } else {
    return s.substring(0, s.indexOf('?'));
  }
}

// 獲取參數
Map<String, dynamic> _getParamsStr(String s) {
  if (s.indexOf('?') == -1) {
    return Map();
  } else {
    return json.decode(s.substring(s.indexOf('?') + 1));
  }
}

Widget _widgetForRoute(String url) {
  String route = _getRouteName(url);
  Map<String, dynamic> params = _getParamsStr(url);
  switch (route) {
    default:
      return MaterialApp(
        theme: ThemeData(
          primaryColor: Color(0xFF008577),
          primaryColorDark: Color(0xFF00574B),
        ),
        home: HomePage(route, params),
      );
  }
}

home_page.dart

import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  String route;
  Map<String, dynamic> params;

  HomePage(this.route, this.params);

  @override
  State<StatefulWidget> createState() {
    return _HomePageState();
  }
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter頁面'),
        automaticallyImplyLeading: false,
      ),
      body: Center(
        child: Text('首頁'),
      ),
    );
  }
}

ok,Demo代碼到這裡就寫完了,然後信心滿滿的run起來,發現直接崩了。這就是我要跟你說的其中一個坑,so架構的問題:
在這裡插入圖片描述
大部分老項目工程中用到的是armeabi架構,但flutter最低支持到armeabi-v7a,如果不做特殊處理,就會出現上面的Crash。怎麼辦?解決辦法自然有,就是找到flutter module工程的構建物,把armeabi-v7a下的libFlutter.so拿出來,放到原生工程的armeabi下,我寫了個shell腳本,然後通過Hook Gradle Task的方式插入到編譯流程中去。

copyFlutterSo.sh

#!/bin/bash

# 當前目錄
CURRENT_DIR="`pwd`"
# 當前build目錄,具體以工程為準
BUILD_DIR="`pwd`/build"
# gradle 5.6.2 armeabi so路徑
#ARMEABI_DIR="$BUILD_DIR/intermediates/merged_native_libs/debug/out/lib/armeabi"
# gradle 4.10.1 armeabi so路徑
ARMEABI_DIR="$BUILD_DIR/intermediates/transforms/mergeJniLibs/$1/0/lib/armeabi"
# armeabi-v7a so存放路徑
ARMEABI_V7A_DIR="$BUILD_DIR/intermediates/transforms/mergeJniLibs/$1/0/lib/armeabi-v7a"

echo -e "\033[47;30m ========== copy $1 libflutter.so ========== \033[0m"
if [[ "$1" == "debug" ]]; then
  # 將libflutter.so copy到armeabi架構中去
  cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR}
  echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}"

elif [[ "$1" == "profile" ]]; then
  # 將libflutter.so copy到armeabi架構中去
  cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR}
  # 將libapp.so也copy到armeabi架構中去
  cp -rf ${ARMEABI_V7A_DIR}/libapp.so ${ARMEABI_DIR}
  echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}"
  echo "copy ${ARMEABI_V7A_DIR}/libapp.so to ${ARMEABI_DIR}"


elif [[ "$1" == "release" ]]; then
  # 將libflutter.so copy到armeabi架構中去
  cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR}
  # 將libapp.so也copy到armeabi架構中去
  cp -rf ${ARMEABI_V7A_DIR}/libapp.so ${ARMEABI_DIR}
  echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}"
  echo "copy ${ARMEABI_V7A_DIR}/libapp.so to ${ARMEABI_DIR}"
fi

Hook Gradle Task

afterEvaluate { project ->
        android.applicationVariants.each { variant ->
           /**
             * 由於flutter不支持armeabi,此處在merge(Debug|Profile|Release)NativeLibs與strip(Debug|Profile|Release)DebugSymbols之間插入一個任務,
             * 將libflutter.so和libapp.so拷貝到merged_native_libs/(debug|profile/release)/out/lib/armeabi目錄下,使它們能打到最終的apk里。
             *
             * 詳情見copyFlutterSo.sh
             */
            def taskPostfix = variant.name.substring(0, 1).toUpperCase() +
                    variant.name.substring(1)
            project.task("copyFlutterSo$taskPostfix") {
                doLast {
                    exec {
                        // 執行shell腳本
                        commandLine "sh", "./copyFlutterSo.sh", variant.name
                    }
                }
            }
            // 註意這個是在gradle 5.6.2版本的task
//            project.tasks["copyFlutterSo$taskPostfix"].dependsOn(project.tasks["merge$taskPostfix" + "NativeLibs"])
//            project.tasks["strip$taskPostfix" + "DebugSymbols"].dependsOn(project.tasks["copyFlutterSo$taskPostfix"])
//
            // gradle 4.10.1,註意插入task的依賴順序
            project.tasks["copyFlutterSo${taskPostfix}"].dependsOn(project.tasks["transformNativeLibsWithMergeJniLibsFor${taskPostfix}"])
            project.tasks["process${taskPostfix}JavaRes"].dependsOn(project.tasks["copyFlutterSo$taskPostfix"])
        }
    }
}

這樣我們每次執行assembleDebug 或者assembleRelease都能自動將對應的armeabi-v7alibflutter.solibapp.so複製到armeabi下。

然後再run一次,這個時候就真正把我們的混合工程跑起來了。

工程最佳實踐

這裡我提我們目前的做法:

  1. flutter module工程單獨以git倉庫托管
  2. 以submodule的方式將flutter module工程管理
  3. 調整依賴路徑如下:
// 引入flutter module
setBinding(new Binding([gradle: this]))// new
// module工程和setting.gradle文件同級
evaluate(new File(                                                      // new
        settingsDir,                                               // new
        'edu_flutter_module/.android/include_flutter.groovy'                          // new
))

include ':edu_flutter_module'
project(':edu_flutter_module').projectDir = new File('edu_flutter_module')

主要改動是將module工程和setting.gradle文件同級.

  1. 通過持續構建系統搭建Flutter構建環境,滿足日常開發構建

總結

以module方式接入Flutter適合大部分存量的項目,目前我們項目已經以這種方式跑起來並且打通持續構建,目前已經踩了部分坑,總得來說經過這段時間對Flutter這個框架的實踐我們團隊已經掌握的新技術棧去為業務賦能,接下來的工作就是不斷提升和優化新的研發體驗,讓統一技術棧這個目標不是說說而已。未來也將會輸出更多乾貨,幫助業內的朋友也能加入到終端的研發變革中來。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • create database 課程管理 //1:create database為SQL語句,用於創建資料庫。執行完之後會創建一個新資料庫及存儲該資料庫的文件,或從先前創建的資料庫文件中附加資料庫。 2:資料庫名稱在伺服器中必須唯一,並且符合標識符的規則。使用一條create database語句即 ...
  • MySQL補充——忘記密碼怎麼辦 摘要:本文主要記錄了在忘記密碼時怎麼辦。 部分內容來自以下博客: https://www.cnblogs.com/wuotto/p/9682400.html 關閉MySQL資料庫 使用命令檢查MySQL資料庫是否已經關閉: 1 [root@localhost ~]# ...
  • DELETE FROM Persons WHERE Id NOT IN (SELECT MIN(Id)AS id FROM Persons GROUP BY Email) ; You can't specify target table 'Person' for update in FROM cla ...
  • MySQL補充——獲取自增主鍵的下一個自增值 摘要:本文主要學習瞭如何獲得自增主鍵的下一個值。 格式 1 select auto_increment from information_schema.tables where table_schema='資料庫名' and table_name='表名 ...
  • 文章前言 提到記憶體管理,我們就需要考慮Redis的記憶體過期策略和記憶體淘汰機制。該文章便從這兩方面入手,分享一些在Redis記憶體方面相關的基礎知識。 文章中使用的示例版本為Redis5.0版本。 記憶體過期策略 記憶體過期策略主要的作用就是,在緩存過期之後,能夠及時的將失效的緩存從記憶體中刪除,以減少記憶體的 ...
  • 本文更新於2019-06-16,使用MySQL 5.7,操作系統為Deepin 15.4。 常用字元集 字元集 定長 代碼寬度 說明 ASCII或ISO-646 是 1位元組7位 英文字母、數字、標點符號和33個控制符 ISO-8859系列 是 1位元組8位 各西歐字元集,相容ASCII GB2312 ...
  • 前一陣領導安排了一個任務:定時將集團資料庫某表的數據同步至我們公司伺服器的資料庫,感覺比寫增刪改查SQL有趣,特意記錄下來,希望能幫到有類似需求的小伙伴,如有錯誤也希望各位不吝指教 環境描述: 集團資料庫:Oracle 11g 部門資料庫:Oracle 11g 使用的軟體:PLSQL Develop ...
  • 教程 美團外賣Flutter動態化實踐 插件 native-draggable Native Drag and Drop for Flutter on iOS and MacOS flutter-mvvm Flutter plugin to rapidly create a Page with MV ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...