Android Flutter混合開發問題總結

JavaNoober發表於2019-09-23

最近一直在探索android flutter的混合開發,一路遇到了很多的坑,接下來便將這些記錄下來,希望能幫助到大家的開發。

flutter常用指令

  • flutter help create
    flutter建立專案的指令。我們可以指定建立module還是project,可以指定語言,如java、kotlin,同時可以指定是否生成androidx專案。

      -h, --help                     Print this usage information.
          --[no-]pub                 Whether to run "flutter pub get" after the project has been created.
                                     (defaults to on)
      
          --[no-]offline             When "flutter pub get" is run by the create command, this indicates whether to run it in offline mode or not. In offline mode, it will need to have all dependencies already available in the pub cache to succeed.
          --[no-]with-driver-test    Also add a flutter_driver dependency and generate a sample 'flutter drive' test.
      -t, --template=<type>          Specify the type of project to create.
      
                [app]                (default) Generate a Flutter application.
                [package]            Generate a shareable Flutter project containing modular Dart code.
                [plugin]             Generate a shareable Flutter project containing an API in Dart code with a platform-specific implementation for Android, for iOS code, or for both.
      
      -s, --sample=<id>              Specifies the Flutter code sample to use as the main.dart for an application. Implies --template=app. The value should be the sample ID of the desired sample from the API documentation website (http://docs.flutter.dev). An example can
                                     be found at https://master-api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html
      
          --list-samples=<path>      Specifies a JSON output file for a listing of Flutter code samples that can created with --sample.
          --[no-]overwrite           When performing operations, overwrite existing files.
          --description              The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.
                                     (defaults to "A new Flutter project.")
      
          --org                      The organization responsible for your new Flutter project, in reverse domain name notation. This string is used in Java package names and as prefix in the iOS bundle identifier.
                                     (defaults to "com.example")
      
          --project-name             The project name for this new Flutter project. This must be a valid dart package name.
      -i, --ios-language             [objc, swift (default)]
      -a, --android-language         [java, kotlin (default)]
          --[no-]androidx            Generate a project using the AndroidX support libraries
    複製程式碼
  • flutter channel

檢視當前flutter分支

Flutter channels:
  beta
  dev
  master
* stable
複製程式碼
  • flutter doctor

診斷當前flutter配置是否有問題

  • flutter doctor

診斷當前flutter配置是否有問題

  • flutter version --force 1.0.0

切換到指定flutter版本

  • flutter clean

clean當前工程

  • flutter run

執行當前工程

  • flutter analyze

分析當前專案dart程式碼

  • flutter assemble

構建資源

  • flutter attach

attach當前專案,通常用於除錯

  • flutter bash-completion

Output command line shell completion setup scripts.

  • flutter build

編譯flutter專案

  • flutter config

配置flutter設定

  • flutter devices

檢視裝置列表

  • flutter drive

為當前專案執行驅動測試

  • flutter emulators

檢視模擬器列表以及執行

  • flutter format

格式化dart程式碼

  • flutter install

安裝flutter專案

  • flutter logs

檢視執行日誌

  • flutter screenshot

對裝置進行截圖

  • flutter test

執行flutter單元測試

  • flutter upgrade

更新flutter 版本

  • flutter version

檢視flutter 版本

flutter常見問題的解決

  • flutter工程無法進行依賴

可以檢視一下.android/Flutter/build.gradle的buildTypes中是否包含app的build.gradle中所需的buildTypes

  • 缺少libflutter.so

flutter專案預設支援的是armeabi-v7a,所以需要在app的gradle中加入如下配置:

    ndk {
        abiFilters "armeabi-v7a"
    }  
複製程式碼

如果還是提示這個錯誤,則需要在gradle.properties中華加入如下配置:

target-platform=android-arm
複製程式碼
  • 編譯apk成功,但是執行flutter失敗,例如:VM snapshot must be valid. /Check failed: vm. Must be able to initialize the VM.

可以嘗試flutter clean, 然後flutter run一下專案,再去執行主工程

  • flutter混淆配置

      -keep class io.flutter.app.** {*;}
      -keep class io.flutter.plugin.** {*;}
      -keep class io.flutter.util.** {*;}
      -keep class io.flutter.view.** {*;}
      -keep class io.flutter.** {*;}
      -keep class io.flutter.plugins.** {*;}
      -dontwarn io.flutter.**
    複製程式碼
  • 跳轉flutter所在activity黑屏

      val flutterView = Flutter.createView(this, lifecycle, Gson().toJson(map))
      val layout = findViewById<FrameLayout>(R.id.flutter_container)
      layout.visibility = View.INVISIBLE
      layout.addView(flutterView)
      val listeners = arrayOfNulls<FlutterView.FirstFrameListener>(1)
      listeners[0] = FlutterView.FirstFrameListener {
          layout.visibility = View.VISIBLE
          loadingDialog.dismiss()
      }
      flutterView.addFirstFrameListener(listeners[0])
    複製程式碼

或者設定當前主題

  • flutter的activity進入載入較慢

debug包這種情況比較明顯,但是release載入很快,可以仿照一下閒魚,在進入FlutterActivity的時候提供一個載入loading

Activity中載入flutter幾種方式

直接載入flutter的main.dart

GeneratedPluginRegistrant.registerWith即可

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }
}
複製程式碼

繼承FlutterActivity

class TestFlutterActivity : FlutterActivity() {

    private lateinit var flutterFragment: FlutterFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_flutter_test)
        GeneratedPluginRegistrant.registerWith(this)
    }


    override fun createFlutterView(context: Context?): FlutterView {
        val matchParent = WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        val nativeView = this.createFlutterNativeView()
        val flutterView = FlutterView(this, null, nativeView)
        flutterView.setInitialRoute("web_view_page")
        flutterView.layoutParams = matchParent
//        flutterView.enableTransparentBackground()
        val layout = findViewById<FrameLayout>(R.id.flutter_container)
        val listeners = arrayOfNulls<FlutterView.FirstFrameListener>(1)
        listeners[0] = FlutterView.FirstFrameListener {
            layout.visibility = View.VISIBLE
        }
        flutterView.addFirstFrameListener(listeners[0])
        this.addContentView(flutterView, matchParent)
        return flutterView
    }

    override fun onBackPressed() {
        if (flutterView != null) {
            flutterView.popRoute()
        } else {
            super.onBackPressed()
        }
    }
}
複製程式碼

addContentView直接載入FlutterView

class MainFlutterActivity : FlutterFragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main_flutter)
        val mFlutterView: View = Flutter.createView(this, lifecycle, "main_flutter")
        val mParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT)
        addContentView(mFlutterView, mParams)
    }
}
複製程式碼

通過FlutterFragment

class FlutterActivity : AppCompatActivity() {

    private lateinit var flutterFragment: FlutterFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_helper_center)
        flutterFragment = Flutter.createFragment("web_view_page")
        supportFragmentManager.beginTransaction().replace(R.id.flutter_container, flutterFragment).commitAllowingStateLoss()
    }
}
複製程式碼

Android原生與Flutter間的通訊、傳值

可以通過flutter_boost,但是目前有適配問題,不支援flutter1.7、1.9

Flutter傳值->Android

有點類似於Eventbus的方式:

flutter.dart中傳送事件:

  static const platform = const MethodChannel("webview");

  void _finish() {
    platform.invokeMethod("finish");
  }
複製程式碼

android接收並處理:

    MethodChannel(flutterView, "webview").setMethodCallHandler { methodCall, result ->
        if(methodCall.method == "finish"){
            finish()
        }
    }
複製程式碼

Android傳值->Flutter

android跳轉flutter的時候可以通過路由名字直接傳入值。

dart:

import 'dart:ui' as ui;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  static const String HELP_URL =
      'https://xxxx';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or press Run > Flutter Hot Reload in a Flutter IDE). Notice that the
        // counter didn't reset back to zero; the application is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: _widgetRouter(ui.window.defaultRouteName)
    );
  }
}

Widget _widgetRouter(String json) {
  return RouterManager.getInstance().getPageByRouter(json);
}


class RouterManager {
  static RouterManager mInstance = new RouterManager();

  static RouterManager getInstance() {
    return mInstance;
  }

  /*
    根據傳入的route名  找到對應跳轉的路徑
   */
  StatefulWidget getPageByRouter(String jsonStr) {
    String path = "";
    String param = "";
    if (jsonStr != null && jsonStr.isNotEmpty && jsonStr != "/") {
      var jsonResponse = jsonDecode(jsonStr);
      path = jsonResponse["path"];
      print("==== router === path = $path");
      param = jsonResponse["param"];
      print("==== router === param = $param");
      path = path != null && path.isNotEmpty ? path : RouterPath.HOME_PAGE;
      param = param != null && param.isNotEmpty ? param : "[]";
    }
    Map<String, dynamic> map = json.decode(param);
    Widget widget = _getWidgetPage(path, map);

    if (widget == null) {
      debugPrint('==== 找不到widget =====');
    }
    return widget;
  }

  Widget _getWidgetPage(String path, Map<String, dynamic> map) {
    Widget widget;
    switch (path) {
      case RouterPath.HOME_PAGE:
        widget = MyHomePage(title: 'Flutter Demo Home Page');
        break;
      case RouterPath.WEB_VIEW_PAGE:
        widget = WebViewPage(map["title"], map["url"]);
        break;
    }
    return widget;
  }
}

class RouterPath {
  static const String HOME_PAGE = 'home_page';
  static const String WEB_VIEW_PAGE = 'web_view_page';
}
複製程式碼

在_getWidgetPage內配置路由跳轉的值及對應介面。

android

與之前主要的區別在於引數通過hashmap轉換成json形式放入路由名稱中,然後把路由名連同引數一起傳給main.dart

    val map = HashMap<String, String>()
    val params = HashMap<String, String>()
    params.put("title", "幫助中心")
    params.put("url", WebUrlConfig.HELP_URL)
    map.put("path", "web_view_page")
    map.put("param", Gson().toJson(params))
    val matchParent = WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
    val nativeView = this.createFlutterNativeView()
    val flutterView = FlutterView(this, null, nativeView)
    flutterView.setInitialRoute(Gson().toJson(map))
    flutterView.layoutParams = matchParent
    val layout = findViewById<FrameLayout>(R.id.flutter_container)
    val listeners = arrayOfNulls<FlutterView.FirstFrameListener>(1)
    listeners[0] = FlutterView.FirstFrameListener {
        layout.visibility = View.VISIBLE
    }
    flutterView.addFirstFrameListener(listeners[0])
    this.addContentView(flutterView, matchParent)
複製程式碼

android部分的封裝

我這裡對其android部分進行了封裝,需要使用的話直接拷貝程式碼即可:

BaseFlutterActivity:

abstract class BaseFlutterActivity : AppCompatActivity() {

    lateinit var flutterView: FlutterView

    private val loadingDialog: LoadingDialog by lazy {
        LoadingDialog(this, "載入中")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flutter_activity)
        loadingDialog.show()
        val map = HashMap<String, String>()
        map["path"] = routeUrl()
        map["param"] = Gson().toJson(routeParams())
        flutterView = Flutter.createView(this, lifecycle, Gson().toJson(map))
        val layout = findViewById<FrameLayout>(R.id.flutter_container)
        layout.addView(flutterView)
        val listeners = arrayOfNulls<FlutterView.FirstFrameListener>(1)
        listeners[0] = FlutterView.FirstFrameListener {
            layout.visibility = View.VISIBLE
            loadingDialog.dismiss()
        }
        flutterView.addFirstFrameListener(listeners[0])

        if (setChannel().isNotEmpty()) {
            setFlutterMessageHandler()?.let {
                MethodChannel(flutterView, setChannel()).setMethodCallHandler(it)
            }
        }

    }

    open fun setChannel(): String {
        return ""
    }

    open fun setFlutterMessageHandler(): MethodChannel.MethodCallHandler? {
        return null
    }


    abstract fun routeUrl(): String

    abstract fun routeParams(): HashMap<String, String>
}
複製程式碼

使用:
下面程式碼是跳轉一個webview並且傳入url:

class FlutterWebActivity : BaseAegisFlutterActivity() {

    private val title: String by lazy { intent.getStringExtra(TITLE) }
    private val url: String by lazy { intent.getStringExtra(URL) }

    companion object {
        const val TITLE = "title"
        const val URL = "url"

        fun buildIntent(context: Context?, title: String, url: String): Intent {
            val intent = Intent(context, FlutterWebActivity::class.java)
            intent.putExtra(TITLE, title)
            intent.putExtra(URL, url)
            return intent
        }
    }

    override fun routeUrl(): String = "web_view_page"

    override fun routeParams(): HashMap<String, String> {
        val params = HashMap<String, String>()
        params["title"] = title
        params["url"] = url
        return params
    }

    override fun setChannel(): String = "webview"

    override fun setFlutterMessageHandler(): MethodChannel.MethodCallHandler? {
        return MethodChannel.MethodCallHandler { methodCall, _ ->
            if (methodCall.method == "finish") {
                finish()
            }
        }
    }
}
複製程式碼

總結

Android Flutter的混合開發基礎先總結到這裡,希望能幫助到大家。後續內容我會再進行補充。

相關文章