Flutter集成到Android項目三部曲

來源: Android 團隊 - 餘自然android

本文主要解決3個問題:ios

  1. 集成Flutter到Android項目,能夠打開Flutter的默認頁面
  2. 能夠跳轉到Flutter的指定頁面
  3. 能夠將Flutter的指定組件嵌入到原生頁面,並傳遞參數

1.集成Flutter到Android

這裏,咱們以Flutter Module建立一個Flutter工程(flutter),而後run起來,就能夠在.android/Flutter/build/outouts/aar文件夾下面獲得這個aar git

這裏之因此以Flutter Module模式開發,而不是Flutter Application,就是爲了獲得這個aar。 Flutter Module模式下自動生成的.android文件夾下,纔會有這個Flutter文件夾,Flutter Application則沒有。 這樣的話,咱們才能夠借用Flutter已經有的生成aar的gradle腳本,否則還得本身去寫gradle打包腳本,很容易踩到坑裏就爬不起來了。github

而後咱們再另開一個窗口,新建一個Android工程(flutter_container),將這個aar複製過去 bash

這裏須要注意的一個問題,由於Flutter自己緣由,致使複製出來的aar裏面缺乏icudtl.dat文件,須要咱們本身手動複製這個icudtl.dat文件到assets/flutter_shared目錄下。app

怎麼獲得這個icudtl.dat文件呢,很簡單,解壓Flutter工程生成的默認apk便可獲得 less

而後,咱們就須要在宿主Android工程裏面,創建接收Flutter的Activity了。這裏能夠借鑑Flutter工程的.android/app目錄,核心就是兩個:async

  1. Application:初始化Flutter
public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }
}
複製代碼
  1. Activity:繼承FlutterActivity
/**
 * debug模式原生跳轉到flutter界面會出現白屏,release包就不會出現白屏了
 */
public class MainFlutterActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }
}
複製代碼

這樣之後,咱們就能夠跳轉這個MainFlutterActivity,實如今Android工程裏面進入Flutter工程的默認頁面了。ide

2. 跳轉指定頁面

上面只是簡單集成了Flutter,可是咱們知道,咱們從Android工程裏面跳轉Flutter,確定是須要選擇性的跳轉指定頁面的,不可能只是簡單的跳轉默認頁面就完了,因此,這裏須要用到Flutter的靜態路由了。佈局

修改Flutter工程的main.dart,定義了兩個指定頁面的路由:homePage、channelPage

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TestPage(),
      //這種方式不能傳遞參數,主要是方便原生調用
      routes: <String, WidgetBuilder> {
        'homePage': (BuildContext context) => new HomePage(),
        'channelPage': (BuildContext context) => new ChannelPage(),
      },
    );

  }
}
複製代碼

而後在宿主Android工程下,添加指定頁面的容器Activity,經過Flutter.createView來獲取指定頁面的View

注意,這裏的HomeFlutterActivity只須要繼承AppCompatActivity 便可,不須要繼承FlutterActivity了。

public class HomeFlutterActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FlutterView homePage = Flutter.createView(
                this,
                getLifecycle(),
                "homePage"
        );
        setContentView(homePage);
    }
}
複製代碼

這樣之後,咱們就能夠跳轉這個HomeFlutterActivity,實如今Android工程裏面進入Flutter工程的指定頁面了。

3. 嵌入View並傳遞參數

上面雖然可以跳轉指定頁面了,可是很顯然,有一個很大的問題:不能傳遞參數。

這是Flutter的靜態路由的一個很大的弊端,雖然經過動態路由能夠傳遞參數和接收返回值,可是動態路由無法給原生調用。

Navigator.of(context)
                .push<String>(new MaterialPageRoute(builder: (context) {
              return new NextPage(params);
            })).then((String value) {
              setState(() {
                params = value;
              });
            });
複製代碼

有一個Flutter的路由庫:Fluro,能夠實現靜態路由傳參,例如這樣:

傳參

var bodyJson = '{"user":1281,"pass":3041}';
router.navigateTo(context, '/home/$bodyJson');
複製代碼

接收

Router router = new Router();

void main() {
  router.define('/home/:data', handler: new Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
        return new FluroHomePage(params['data'][0]);
      }));
  runApp(MyApp());
}
複製代碼

可是,這種方式在Flutter內部還行,卻沒法給原生調用,在原生裏面經過Flutter.createView的時候,是無法使用Fluro的,只能是默認的路由。

調研了不少方案,最後,沒有辦法了,只好採用最笨的方法:經過MethodChannel來傳遞參數。

這裏須要注意的是MethodChannel的調用,應該FlutterView已經建立完成,因此須要經過flutterView.post(new Runnable())來執行了,直接執行是不會傳參給Flutter的。

原生傳參給Flutter

原生調用

MethodChannel channel = new MethodChannel(flutterView, CHANNEL);
channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
    @Override
    public void success(@Nullable Object o) {
        Log.i("flutter","1.原生調用invokeFlutterMethod-success:"+o.toString());
    }
    @Override
    public void error(String s, @Nullable String s1, @Nullable Object o) {
        Log.i("flutter","1.原生調用invokeFlutterMethod-error");
    }
    @Override
    public void notImplemented() {
        Log.i("flutter","1.原生調用invokeFlutterMethod-notImplemented");
    }
});
複製代碼

Flutter執行

platform.setMethodCallHandler((handler) {
      Future<String> future=Future((){
        switch (handler.method) {
          case "invokeFlutterMethod":
            String args = handler.arguments;
            print("2.Flutter執行invokeFlutterMethod:${args}");
            return "this is flutter result";
        }
      });
      return future;
    });
複製代碼

Flutter傳參給原生

Flutter調用

print("3.Flutter調用invokeNativeMethod");
int result =
    await platform.invokeMethod("invokeNativeMethod", "hello,native");
print("5.收到原生執行結果:${result}");
複製代碼

原生執行

channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
    @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        switch (call.method) {
            case "invokeNativeMethod":
                String args = (String) call.arguments;
                Log.i("flutter","4.原生執行invokeNativeMethod:"+args);
                result.success(200);
                break;
            default:
        }
    }
});
複製代碼

最後貼一下這個傳參頁面的完整代碼吧,主要就是跑了一下:

  1. 原生調用invokeFlutterMethod
  2. Flutter執行invokeFlutterMethod
  3. Flutter調用invokeNativeMethod
  4. 原生執行invokeNativeMethod

Android:

public class ChannelFlutterActivity extends AppCompatActivity {

    private static final String CHANNEL = "com.ezbuy.flutter";

    FlutterView flutterView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_channel);
        FrameLayout frFlutter = findViewById(R.id.fr_flutter);
        flutterView = getFlutterView("channelPage");
        frFlutter.addView(flutterView);
        flutterView.post(new Runnable() {
            @Override
            public void run() {
                initMethodChannel(flutterView);
            }
        });
    }

    public FlutterView initMethodChannel(FlutterView flutterView) {
        MethodChannel channel = new MethodChannel(flutterView, CHANNEL);
        //1.原生調用Flutter方法
        channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
            @Override
            public void success(@Nullable Object o) {
                Log.i("flutter","1.原生調用invokeFlutterMethod-success:"+o.toString());
            }

            @Override
            public void error(String s, @Nullable String s1, @Nullable Object o) {
                Log.i("flutter","1.原生調用invokeFlutterMethod-error");
            }

            @Override
            public void notImplemented() {
                Log.i("flutter","1.原生調用invokeFlutterMethod-notImplemented");
            }
        });
        Log.i("flutter","1.原生調用invokeFlutterMethod");
        //4.Flutter調用原生方法的監聽
        channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                switch (call.method) {
                    case "invokeNativeMethod":
                        String args = (String) call.arguments;
                        Log.i("flutter","4.原生執行invokeNativeMethod:"+args);
                        result.success(200);
                        break;
                    default:
                }
            }
        });
        return flutterView;
    }

    public FlutterView getFlutterView(String initialRoute) {
        return Flutter.createView(
                this,
                getLifecycle(),
                initialRoute
        );
    }
}
複製代碼

Flutter

class ChannelPage extends StatefulWidget {

  ChannelPage();

  @override
  _ChannelPageState createState() => _ChannelPageState();
}

class _ChannelPageState extends State<ChannelPage> {
  static const platform = const MethodChannel('com.ezbuy.flutter');

  String data;

  @override
  void initState() {
    super.initState();
    data ="默認data";
    initChannel();
  }

  @override
  Widget build(BuildContext context) {
    //必須用Scaffold包裹
    return Scaffold(body: new Center(child: new Text(data)));
  }

  void initChannel() {
    platform.setMethodCallHandler((handler) {
      Future<String> future=Future((){
        switch (handler.method) {
          case "invokeFlutterMethod":
            String args = handler.arguments;
            print("2.Flutter執行invokeFlutterMethod:${args}");
            setState(() {
              data = "2.Flutter執行invokeFlutterMethod:${args}";
            });
            invokeNativeMethod();
            return "this is flutter result";
        }
      });
      return future;
    });
  }

  void invokeNativeMethod() async {
    print("3.Flutter調用invokeNativeMethod");
    int result =
        await platform.invokeMethod("invokeNativeMethod", "hello,native");
    print("5.收到原生執行結果:${result}");
  }

}
複製代碼

對啦,咱們這節說的是將Flutter以View級別嵌套在一個Android的Activity裏面,其實很簡單了啊,由於咱們經過Flutter.createView建立出來的View和普通的View並無什麼太大的區別,直接addView就能夠了,沒啥特別操做,好比這個ChannelFlutterActivity,我用的佈局文件就是以下所示:

最後的執行效果就是:

其它坑

1. Flutter工程依賴了插件時,宿主Android工程會報找不到插件的原生代碼的錯誤

個人Flutter工程依賴了shared_preferences插件,致使報錯:

緣由是:Flutter工程導出成aar的時候,沒有包含插件裏面的原生代碼。

解決方案有2種,網上說是不用默認的生成aar的方式,用fataar-gradle-plugin來讓生成的flutter.aar直接包含嵌套的插件工程的aar,這就須要修改Flutter工程的.android/Flutter/build.gradle文件了。我試過,結果報了循環依賴的錯誤,就放棄了,你們若是這個方案走通了,歡迎告知我具體步驟。

個人解決方案:這裏我採起了一個簡單粗暴直接的方案,直接找到插件的aar,將它也複製到宿主Android工程了。這個插件的aar在這裏:

複製到這裏:

可是這個方案的弊端就是,之後每個插件,你都須要複製一下,後期的維護成本是有點高的。不像fataar是一勞永逸,只有flutter.aar這一份aar的。尤爲是後期確定會將aar作成遠程依賴,而再也不是直接發覆制過去,那維護成本就更高了些。

結語

經過上文能夠看到,其實Flutter集成到Android項目仍是挺方便的(除了FlutterView傳參有點麻煩)。至於Flutter如何集成到ios項目,我尚未實踐過,還須要和ios的同事探索,若是你在集成到ios項目的過程當中,填了哪些坑,有哪些經驗總結,歡迎和咱們交流。

相關文章
相關標籤/搜索