# Android項目中嵌入Flutter項目以及各種踩坑(持續更新)

[toc]
純筆記,非技術博客

分類

Android原生項目內嵌Flutter項目現在有兩種途徑:源碼集成以及產物集成
這裏借下圖來說明:

因爲我是隻有一個人開發,所以我想先試下源碼集成,後續有機會也會試下產物集成

源碼集成

新建安卓項目

這一部就不贅述了,安卓狗都知道的

新建flutter目錄

這裏有些分歧,有些人是直接在android目錄裏面新建了Flutter的module,有些人是把Flutter新建在和Module相齊的目錄,其實應該是後者比較規範一點,畢竟還有ios模塊要用嘛。

不過我只有安卓,就還是用普通module的方式吧。

Android Studio創建

這裏有2種創建Flutter模塊的方法,選自己喜歡的吧

在androidStudio中新建Flutter Module

稍等as一頓操作,這樣就可以新建一個Flutter Module了

在命令行創建Flutter Module
flutter create -t module {moduleName}

比如創建一個叫ly_module

flutter create -t module ly_module
在android項目中關聯Flutter Module
include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
        'xxxxx/.android/include_flutter.groovy'
))
在app的build.gradle裏面加入對應依賴
implementation project(':flutter')
新建Flutter模塊入口

其實就是新建了一個activity,在裏面丟進去flutter模塊:

/**
 * Author: Ly
 * Data:2019/4/23-15:49
 * Description:
 */
class FlutterEntryActivity : AppCompatActivity() {
    companion object {
        fun toFlutterEntryActivity(a: Activity) {
            a.startActivity(Intent(a, FlutterEntryActivity::class.java))
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flutter_entry)
        val flutterView = Flutter.createView(this, lifecycle, "Ly")
        val layout = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        addContentView(flutterView, layout)
    }
}

這裏在debug模式有個問題:
雖然加入了一個view是很快速的問題,但是初始化也是要時間的,所以在debug模式下,會有一定時間的黑屏時間(測試在release模式下是很ok的,果然flutter的debug模式和release模式是完全不一樣的HHHHHH
這裏找了有三個解決方法,我丟在下面的踩坑上面去。

flutter處理

我們上面進入flutter的時候傳了3個參數,我們注意下第三個Ly,對應的是一個路由地址,
這個我們在flutter中可以拿到

window.defaultRouteName

所以我們在main.dart中可以有如下處理:

void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    case "Ly":
      return MyApp();
    default:
      return MyApp();
  }
}

這裏可以根據路由地址跳轉到對應的app地址

以上步驟很多博客都可以看到,接下來我們來講下容易踩坑的地方

分支問題

現在的時間是2019年4月23日16點53分,版本信息如下:


切記一定要切到Master分支

settings.gradle報錯

很多時候會發生setting.gradle 我們加的

include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
        'xxxxx/.android/include_flutter.groovy'
))

報錯,我覺得有2個點可以排查

  1. 目錄路徑沒有錯
  2. .anroid/文件存在,如上文所說的命令行創建module,其實不會生成.android & .ios 目錄,那這裏的依賴就會報錯了,這個時候可以在flutter窗口下執行
    flutter packages get生成下
項目報錯問題
依賴完畢後,跑項目,會出現
```java
Invoke-customs are only supported starting with Android O
```
這個時候需要在app的build.gradle文件中的*android{)*結點加入
```gradle
 compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
```
debug模式下黑屏
顯示隱藏方法
  1. 一開始的的xml佈局文件設置爲Invisible,注意這裏不能設置爲GONE,因爲兩者有渲染與否的區別
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  android:id="@+id/flFlutterContainer"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:visibility="invisible"
  android:layout_height="match_parent">
</FrameLayout>
  1. 加載完後再顯示出來
        val flutterView = Flutter.createView(this, lifecycle, "Ly")
        flFlutterContainer.addView(flutterView)
        val listeners = arrayOfNulls<FirstFrameListener>(1)
        listeners[0] = FirstFrameListener { flFlutterContainer.visibility = View.VISIBLE }
        flutterView.addFirstFrameListener(listeners[0])
重複設置contentView的方法
```kotlin
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_flutter_entry)
    val flutterView = Flutter.createView(this, lifecycle, "Ly")
    val layout = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
    addContentView(flutterView, layout)
```
渠道打包的問題(這個坑踩了比較久)

因爲我的app是需要打渠道包的,之前也是做了一個很簡陋的渠道包方法,也就是在gradle裏面設置productFlavors,但是這樣在嵌入flutter之後就會出問題了
Check failed: vm. Must be able to initialize the VM.
翻牆翻了好久~也找了很多資料,終於在issues中找到了
相關資料
這裏是因爲加了渠道,flutter_assets資源拷貝路徑出了問題,那既然知道問題了,改吧。
app的build.gradle文件:

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    flavorDimensions ""
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.rongyi.cook"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
//        ndk{abiFilters "armeabi-v7a"}
    }
    productFlavors {

        xiaomi {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "xiaomi",]
        }
        c360 {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "c360",]
        }
        baidu {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "baidu",]
        }
        huawei {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "huawei",]
        }
        yingyongbao {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "yingyongbao",]
        }
        vivo {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "vivo",]
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "\\ 19921208015",]
            minifyEnabled false
        }
    }
    lintOptions {
        disable 'CheckResult'
    }
    // 自定義輸出配置
    android.applicationVariants.all { variant ->
        variant.outputs.all {
            outputFileName = "${variant.name}_${variant.versionName}_${variant.name}.apk"
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation project(':easyly')
    implementation project(':flutter')
}

這裏加入了6個渠道,而對應的,我們也要修改下flutter的build.gradle文件
[圖片上傳失敗...(image-ef6d4f-1556012228778)]
之前的build.gradle:

// Generated file. Do not edit.

def localProperties = new Properties()
def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, 'local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.library'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 28

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

flutter {
    source '../..'
}

dependencies {
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support:support-v13:27.1.1'
    implementation 'com.android.support:support-annotations:27.1.1'
}

修改後的build.gradle

// Generated file. Do not edit.

def localProperties = new Properties()
def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, 'local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.library'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 28
    flavorDimensions ""
    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    productFlavors {

        xiaomi {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "xiaomi",]
        }
        c360 {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "c360",]
        }
        baidu {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "baidu",]
        }
        huawei {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "huawei",]
        }
        yingyongbao {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "yingyongbao",]
        }
        vivo {
            manifestPlaceholders = [TYJ_APP_CHANNEL: "vivo",]
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support:support-v13:27.1.1'
    implementation 'com.android.support:support-annotations:27.1.1'
}

手動控制draw的開關

打開

  // 初始化標題
  AppBar _initAppbar(BuildContext context) {
    return new AppBar(
      backgroundColor: ColorConf.red,
      leading: Builder(
          builder: (context) => IconButton(
                onPressed: () {
                  debugPrint('openDrawer');
                  Scaffold.of(context).openDrawer();
                },
                icon: Icon(
                  Icons.menu,
                  color: ColorConf.colorWhite,
                ),
                tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
              )),
      title: new Row(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Text(
            '當前位置:',
            style: _textStyleForMsg,
          ),
          new Padding(
            padding: const EdgeInsets.only(left: 6),
            child: Text(
              '廣州',
              style: _textStyleForTitle,
            ),
          )
        ],
      ),
      centerTitle: true,
      elevation: 1,
      actions: <Widget>[
        Builder(
          builder: (context) => IconButton(
                onPressed: () {
                  Scaffold.of(context).openEndDrawer();
                },
                icon: Icon(
                  Icons.chat_bubble_outline,
                  color: ColorConf.colorWhite,
                ),
              ),
        )
      ],
    );
  }

關閉

AppBar _initAppBar(BuildContext context) {
  return AppBar(
    backgroundColor: ColorConf.colorRed,
    leading: Builder(builder: (context) =>
        IconButton(icon: Icon(
          Icons.clear,
          color: ColorConf.colorWhite,
        ), onPressed: () {
            Navigator.pop(context);
        })),
  );
}

CustomScrollView 嵌套GridView 滑動衝突

Container _initServiceGriView() {
  List<String> iconsStr = [
    'assets/invite_frends.png',
    'assets/price_standard.png',
    'assets/service_center.png',
    'assets/invite_driver.png',
    'assets/user_feedback.png',
  ];
  List<String> iconsMsg = ['邀請好友', '收費說明', '服務中心', '成爲司機', '用戶反饋'];

  return Container(
    alignment: Alignment.centerLeft,
    margin: const EdgeInsets.only(top: 20, left: 12),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        Container(
          alignment: Alignment.centerLeft,
          child: Text(
            '業務範圍',
            style: _textStyleForTitle,
            textAlign: TextAlign.left,
          ),
        ),
        Container(
          margin: const EdgeInsets.only(top: 20),
          child: GridView.builder(
              shrinkWrap: true,
            primary: false,
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 4, childAspectRatio: 1.0),
              itemCount: iconsMsg.length,
              itemBuilder: (context, index) {
                return Column(
                  children: <Widget>[
                    Image.asset(
                      iconsStr[index],
                      width: 30,
                    ),
                    Container(
                      margin: const EdgeInsets.only(top: 10),
                      child: Text(
                        iconsMsg[index],
                        style: _textStyleForIcon,
                      ),
                    )
                  ],
                );
              }),
        )
      ],
    ),
  );
}

其中兩個屬性:

  1. shrinkWrap 常用於內容大小不確定情況,如果滾動視圖(ListView/GridView/ScrollView 等)沒有收縮包裝,則滾動視圖將擴展到允許的最大大小。如果是無界約束,則 shrinkWrap 必須爲 true。
  2. primary 如果爲 true,即使滾動視圖沒有足夠的內容來支撐滾動,滾動視圖也是可滾動的。否則,默認爲 false 情況下,只有具有足夠內容的用戶才能滾動視圖。

軟鍵盤遮擋輸入框

使用scrollView包裹

class _RegisterPageState extends State<RegisterPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        leading: Icon(
          Icons.arrow_back,
          color: ColorConf.colorWhite,
        ),
        title: Text(
          '註冊',
          style: TextStyle(fontSize: 18, color: ColorConf.colorWhite),
        ),
      ),
      body: LayoutBuilder(
        builder: (BuildContext context,BoxConstraints viewPort){
          return SingleChildScrollView(
            child: ConstrainedBox(constraints: BoxConstraints(
              minHeight: viewPort.maxHeight,
            ),
            child: IntrinsicHeight(
              child: Column(
                children: <Widget>[],
              ),
            ),),
          );
        },
      ),
    );
  }
}

圓形頭像

 Container(
        width: 70,
        height: 70,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          image: DecorationImage(
              image: loginBean == null
                  ? AssetImage('assets/icon_baba2.png')
                  : NetworkImage(
                      'https://ss2.baidu.com/-vo3dSag_xI4khGko9WTAnF6hhy/image/h%3D300/sign=00af05b334f33a87816d061af65d1018/8d5494eef01f3a29f863534d9725bc315d607c8e.jpg'),
              fit: BoxFit.cover),
          border: Border.all(color: ColorConf.colorWhite, width: 2),
        ),
      ),

修改TextField下劃線顏色

TextField(
                  style: StyleComm.styleCommLeft,
                  textAlign: TextAlign.right,
                  decoration: InputDecoration(
                      hintText: _hintMsg,
                      focusedBorder: UnderlineInputBorder(
                          borderSide:
                              BorderSide(color: Colors.lightGreenAccent)),
                      enabledBorder: UnderlineInputBorder(
                          borderSide: BorderSide(color: Colors.yellowAccent)),
                      hintStyle: StyleComm.hintCommRight,
                      border: UnderlineInputBorder(
                        borderSide: BorderSide(
                          color: ColorConf.colorBlack,
                        ),
                      )),
                ),
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章