Flutter集成到Android項目三部曲

作者:余天然

本文主要解決3個問題:

  1. 集成Flutter到Android項目,可以打開Flutter的默認頁面
  2. 可以跳轉到Flutter的指定頁面
  3. 可以將Flutter的指定組件嵌入到原生頁面,并傳遞參數(shù)

1.集成Flutter到Android

這里,我們以Flutter Module創(chuàng)建一個Flutter工程(flutter),然后run起來,就可以在.android/Flutter/build/outouts/aar文件夾下面得到這個aar


這里之所以以Flutter Module模式開發(fā),而不是Flutter Application,就是為了得到這個aar。
Flutter Module模式下自動生成的.android文件夾下,才會有這個Flutter文件夾,F(xiàn)lutter Application則沒有。
這樣的話,我們才可以借用Flutter已經有的生成aar的gradle腳本,不然還得自己去寫gradle打包腳本,很容易踩到坑里就爬不起來了。

然后我們再另開一個窗口,新建一個Android工程(flutter_container),將這個aar復制過去


這里需要注意的一個問題,因為Flutter本身原因,導致復制出來的aar里面缺少icudtl.dat文件,需要我們自己手動復制這個icudtl.dat文件到assets/flutter_shared目錄下。

怎么得到這個icudtl.dat文件呢,很簡單,解壓Flutter工程生成的默認apk即可得到


然后,我們就需要在宿主Android工程里面,建立接收Flutter的Activity了。這里可以借鑒Flutter工程的.android/app目錄,核心就是兩個:

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

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

這樣以后,我們就可以跳轉這個MainFlutterActivity,實現(xiàn)在Android工程里面進入Flutter工程的默認頁面了。

2. 跳轉指定頁面

上面只是簡單集成了Flutter,但是我們知道,我們從Android工程里面跳轉Flutter,肯定是需要選擇性的跳轉指定頁面的,不可能只是簡單的跳轉默認頁面就完了,所以,這里需要用到Flutter的靜態(tài)路由了。

修改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(),
      //這種方式不能傳遞參數(shù),主要是方便原生調用
      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,實現(xiàn)在Android工程里面進入Flutter工程的指定頁面了。

3. 嵌入View并傳遞參數(shù)

上面雖然能夠跳轉指定頁面了,但是很顯然,有一個很大的問題:不能傳遞參數(shù)。

這是Flutter的靜態(tài)路由的一個很大的弊端,雖然通過動態(tài)路由可以傳遞參數(shù)和接收返回值,但是動態(tài)路由沒法給原生調用。

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

有一個Flutter的路由庫:Fluro,可以實現(xiàn)靜態(tài)路由傳參,例如這樣:

傳參

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來傳遞參數(shù)。

這里需要注意的是MethodChannel的調用,應該FlutterView已經創(chuàng)建完成,所以需要通過flutterView.post(new Runnable())來執(zhí)行了,直接執(zhí)行是不會傳參給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執(zhí)行

platform.setMethodCallHandler((handler) {
      Future<String> future=Future((){
        switch (handler.method) {
          case "invokeFlutterMethod":
            String args = handler.arguments;
            print("2.Flutter執(zhí)行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.收到原生執(zhí)行結果:${result}");

原生執(zhí)行

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.原生執(zhí)行invokeNativeMethod:"+args);
                result.success(200);
                break;
            default:
        }
    }
});

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

  1. 原生調用invokeFlutterMethod
  2. Flutter執(zhí)行invokeFlutterMethod
  3. Flutter調用invokeNativeMethod
  4. 原生執(zhí)行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調用原生方法的監(jiān)聽
        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.原生執(zhí)行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執(zhí)行invokeFlutterMethod:${args}");
            setState(() {
              data = "2.Flutter執(zhí)行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.收到原生執(zhí)行結果:${result}");
  }

}

對啦,我們這節(jié)說的是將Flutter以View級別嵌套在一個Android的Activity里面,其實很簡單了啊,因為我們通過Flutter.createView創(chuàng)建出來的View和普通的View并沒有什么太大的區(qū)別,直接addView就可以了,沒啥特別操作,比如這個ChannelFlutterActivity,我用的布局文件就是如下所示:

最后的執(zhí)行效果就是:


其它坑

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

我的Flutter工程依賴了shared_preferences插件,導致報錯:

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

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

我的解決方案:這里我采取了一個簡單粗暴直接的方案,直接找到插件的aar,將它也復制到宿主Android工程了。這個插件的aar在這里:


復制到這里:


但是這個方案的弊端就是,以后每一個插件,你都需要復制一下,后期的維護成本是有點高的。不像fataar是一勞永逸,只有flutter.aar這一份aar的。尤其是后期肯定會將aar做成遠程依賴,而不再是直接發(fā)復制過去,那維護成本就更高了些。

結語

通過上文可以看到,其實Flutter集成到Android項目還是挺方便的(除了FlutterView傳參有點麻煩)。至于Flutter如何集成到ios項目,我還沒有實踐過,還需要和ios的同事探索,如果你在集成到ios項目的過程中,填了哪些坑,有哪些經驗總結,歡迎和我們交流。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容