Flutter學習小計:Android原生項目引入Flutter

前言
目前Flutter可以說是非常火熱了缸匪,多次更新過后也越來越穩(wěn)定翁狐,受到了很多開發(fā)者的青睞。不過純Flutter開發(fā)還是存在一定成本和風險的凌蔬,尤其是對于規(guī)模稍大一些的項目露懒,可能更加適合的是將Flutter用于項目中的某一個模塊,因此我們有必要了解一下如何在原生項目中引入Flutter砂心。

本文介紹一下Android原生項目引入Flutter的方法以及Flutter如何與原生進行交互懈词,包括頁面間的跳轉(zhuǎn)和方法的調(diào)用,本人不懂IOS開發(fā)辩诞,有需要的話還是自行百度吧o(╥﹏╥)o坎弯,但是基本思路我覺得不會差太多的。

Android原生項目中引入Flutter

這應該是目前Flutter在實際開發(fā)中應用最多的一種場景译暂,在已有的Android原生項目中引入Flutter抠忘,針對一些復雜的頁面,使用Flutter開發(fā)可以有效地提高開發(fā)效率外永。
官方提供的文檔Add Flutter to existing apps詳細介紹了原生app引入Flutter的步驟崎脉,不過很遺憾是英文的。我也是參考了網(wǎng)上的一些相關(guān)文章伯顶,總結(jié)了一下文檔中的提到的幾個步驟囚灼。

  • 第一步呛踊、新建Android項目

這沒什么可說的,畢竟我們是要在原生項目中引入Flutter嘛啦撮。

  • 第二步谭网、新建Flutter Module

有兩種方式來創(chuàng)建Flutter Module,第一種是通過命令行來創(chuàng)建赃春,首先切換到Android項目的同級目錄下愉择,執(zhí)行以下命令:

flutter create -t module my_flutter

其中my_flutter為module的名字。第二種是直接使用Android Studio來創(chuàng)建织中,依次點擊左上角的File --> New --> New Flutter Project锥涕,然后選擇Flutter Module。

新建Flutter Module.png

然后填寫module的名稱狭吼、路徑层坠。

最后填寫module的包名,點擊Finish就創(chuàng)建好了一個Flutter Module刁笙。

  • 第三步破花、在Android項目中引入Flutter Module

首先在app下的build.gradle文件中添加以下配置:

compileOptions {
  sourceCompatibility 1.8
  targetCompatibility 1.8
}

我們知道這是使用Java 8所需要的配置,在這里的作用是為了解決版本兼容問題疲吸,如果不配置的話運行項目可能會報錯:Invoke-customs are only supported starting with Android O (--min-api 26)座每。
然后在項目根目錄下的setting.gradle文件中配置:

include ':app'
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
        settingsDir.parentFile,
        'my_flutter/.android/include_flutter.groovy'
))  

記得修改成自己的Flutter Module名稱,之后Sync一下項目摘悴。Binding可能會因為找不到而標紅峭梳,我沒有導包最后也可以Sync成功,并不影響module的引入蹂喻,這一點我還不清楚是什么原因葱椭,如果有知道的小伙伴歡迎提出。
Sync后我們可以看到項目中多了一個名稱為flutter的library module口四,我們需要在app下的build.gradle文件中添加該module的依賴孵运。

implementation project(':flutter')

這樣就成功地將Flutter引入到了Android原生項目中。

Android和Flutter的交互

通過上面的幾個步驟我們已經(jīng)在Android原生項目中集成了Flutter窃祝,之后就需要解決交互問題了掐松。首先介紹一下Android頁面和Flutter頁面之間的跳轉(zhuǎn)踱侣。

Tips:由于Flutter版本的更新粪小,下面介紹的內(nèi)容中存在一些API已經(jīng)被廢棄的情況,不過各種交互場景的處理思路是不變的抡句,關(guān)于Flutter版本變更的內(nèi)容我補充到了文章最后探膊,大家可以結(jié)合起來看。

Android原生頁面跳轉(zhuǎn)Flutter頁面

基本思路就是將Flutter編寫的頁面嵌入到Activity中待榔,官方提供了兩種方式:通過FlutterViewFlutterFragment逞壁,下面我們分別看一下這兩種方式是如何實現(xiàn)的流济。
1.使用FlutterView
首先新建一個Activity,命名為FlutterPageActivity(名稱隨意起)腌闯,在onCreate()方法中添加以下代碼:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 通過FlutterView引入Flutter編寫的頁面
    View flutterView = Flutter.createView(this, getLifecycle(), "route1");
    FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
    layout.leftMargin = 100;
    layout.topMargin = 200;
    addContentView(flutterView, layout);
}

Flutter.createView()方法返回的是一個FlutterView绳瘟,它繼承自View,我們可以把它當做一個普通的View姿骏,調(diào)用addContentView()方法將這個View添加到Activity的contentView中糖声。我們注意到Flutter.createView()方法的第三個參數(shù)傳入了"route1"字符串,表示路由名稱分瘦,它確定了Flutter中要顯示的Widget蘸泻,接下來需要在之前創(chuàng)建好的Flutter Module中編寫邏輯了,修改main.dart文件中的代碼:

import 'dart:ui';
import 'package:flutter/material.dart';

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

Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1':
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter頁面'),
          ),
          body: Center(
            child: Text('Flutter頁面嘲玫,route=$route'),
          ),
        ),
      );
    default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
      );
  }
}

runApp()方法中通過window.defaultRouteName可以獲取到我們在Flutter.createView()方法中傳入的路由名稱悦施,即"route1",之后編寫了一個_widgetForRoute()方法去团,根據(jù)傳入的route字符串顯示相應的Widget抡诞。
最后在MainActivity中添加一個Button,編寫點擊事件土陪,點擊Button跳轉(zhuǎn)到FlutterPageActivity沐绒。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button btnJumpToFlutter = findViewById(R.id.btn_jump_to_flutter);
    btnJumpToFlutter.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, FlutterPageActivity.class);
            startActivity(intent);
        }
    });

運行項目,點擊MainActivity中的Button跳轉(zhuǎn)到FlutterPageActivity旺坠,效果如下圖所示:

可以看到我們已經(jīng)成功地將Flutter編寫的Widget嵌入到了Activity中乔遮,為了更逼真一些,還需要做一些調(diào)整取刃。首先修改LayoutParams參數(shù)蹋肮,將View占滿屏幕。

View flutterView = Flutter.createView(this, getLifecycle(), "route1");
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(flutterView, layout);

然后需要隱藏原生的標題欄璧疗,在資源文件夾res/values中的style.xml文件中添加一個FlutterPageTheme坯辩。

<style name="FlutterPageTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!--狀態(tài)欄透明-->
    <item name="android:windowTranslucentStatus">true</item>
</style>

然后在AndroidManifest.xml文件中設置Activity的Theme穷吮。

<activity
    android:name=".FlutterPageActivity"
    android:theme="@style/FlutterPageTheme" />

再次運行項目看一下效果彤枢,這樣就自然多了骗奖,當然我們還可以繼續(xù)修改標題欄的背景顏色跟啤,這里就不提了管引。


2.使用FlutterFragment
為了簡單庶柿,我們依然使用FlutterPageActivity伟众,新建一個布局文件activity_flutter_page

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/fl_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

修改onCreate()方法:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_flutter_page);
    // 通過FlutterFragment引入Flutter編寫的頁面
    FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
    tx.replace(R.id.fl_container, Flutter.createFragment("route1"));
    tx.commit();
}

Flutter.createFragment()方法傳入的參數(shù)同樣表示路由名稱斤葱,用于確定Flutter要顯示的Widget系瓢,返回一個FlutterFragment阿纤,該類繼承自Fragment,將該Fragment添加到Activity中就可以了夷陋。
在調(diào)試時會遇到一個問題欠拾,顯示出Flutter頁面之前會黑屏幾秒胰锌,不要擔心,打了release包后就沒問題了藐窄。
如何傳遞參數(shù)跳轉(zhuǎn)
通過以上兩種方式實現(xiàn)了將Flutter編寫的頁面嵌入到Activity中资昧,但是這只是最簡單的情況,如果我們需要在頁面跳轉(zhuǎn)時傳遞參數(shù)呢荆忍,如何在Flutter代碼中獲取到原生代碼中的參數(shù)呢榛搔?其實很簡單,只需要在route后面拼接上參數(shù)就可以了东揣,以創(chuàng)建FlutterView的方式為例践惑。

View flutterView = Flutter.createView(this, getLifecycle(),
                "route1?{\"name\":\"StephenCurry\"}");

這里將路由名稱和參數(shù)間用“?”隔開嘶卧,就像瀏覽器中的url一樣尔觉,參數(shù)使用了Json格式傳遞,原因就是方便Flutter端解析芥吟,而且對于一些復雜的數(shù)據(jù)侦铜,比如自定義對象,使用Json序列化也很好實現(xiàn)钟鸵。這時候Flutter端通過window.defaultRouteName獲取到的就是路由名稱+參數(shù)了钉稍,我們需要將路由名稱和參數(shù)分開,這就只是單純的字符串處理了棺耍,代碼如下所示:

String url = window.defaultRouteName;
// route名稱
String route =
    url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
// 參數(shù)Json字符串
String paramsJson =
    url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
// 解析參數(shù)
Map<String, dynamic> params = json.decode(paramsJson);

通過"?"將路由名稱和參數(shù)分開贡未,將參數(shù)對應的Json字符串解析為Map對象,需要導入dart:convert包蒙袍,之后再將參數(shù)傳遞給對應的Widget即可俊卤,這里就不展示了,詳細代碼可以查看Demo害幅。運行效果如下圖所示:

Android原生頁面跳轉(zhuǎn)Flutter頁面

Flutter頁面跳轉(zhuǎn)Android原生頁面

在實現(xiàn)Flutter頁面跳轉(zhuǎn)Android原生頁面之前首先介紹一下Platform Channel消恍,它是Flutter和原生通信的工具,有三種類型:

  • BasicMessageChannel:用于傳遞字符串和半結(jié)構(gòu)化的信息以现,F(xiàn)lutter和平臺端進行消息數(shù)據(jù)交換時候可以使用狠怨。
  • MethodChannel:用于傳遞方法調(diào)用(method invocation),F(xiàn)lutter和平臺端進行直接方法調(diào)用時候可以使用邑遏。
  • EventChannel:用于數(shù)據(jù)流(event streams)的通信佣赖,F(xiàn)lutter和平臺端進行事件監(jiān)聽、取消等可以使用无宿。

這里我就只介紹一下MethodChannel的使用茵汰,它也是我們開發(fā)中最常用的枢里,關(guān)于其他兩種Channel的使用可以自行查閱網(wǎng)上的文章孽鸡。Flutter跳轉(zhuǎn)原生頁面就是通過MethodChannel來實現(xiàn)的蹂午,在Flutter中調(diào)用原生的跳轉(zhuǎn)方法就可以了,接下來我們具體看一下如何實現(xiàn):
1.Android端

// 定義Channel名稱
private static final String CHANNEL_NATIVE = "com.example.flutter/native";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_flutter_page);
    // 通過FlutterView引入Flutter編寫的頁面
    FlutterView flutterView = Flutter.createView(this, getLifecycle(),
            "route1?{\"name\":\"" + getIntent().getStringExtra("name") + "\"}");
    FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    addContentView(flutterView, layout);

    MethodChannel nativeChannel = new MethodChannel(flutterView, CHANNEL_NATIVE);
    nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
            switch (methodCall.method) {
                case "jumpToNative":
                    // 跳轉(zhuǎn)原生頁面
                    Intent jumpToNativeIntent = new Intent(FlutterPageActivity.this, NativePageActivity.class);
                    jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
                    startActivity(jumpToNativeIntent);
                    break;
                default:
                    result.notImplemented();
                    break;
            }
        }
    });
}

首先定義Channel名稱彬碱,需要保證是唯一的豆胸,在Flutter端需要使用同樣的名稱來創(chuàng)建MethodChannel。MethodChannel的構(gòu)造方法有三個參數(shù)巷疼,第一個是messenger晚胡,類型是BinaryMessenger,是一個接口嚼沿,代表消息信使估盘,是消息發(fā)送與接收的工具,由于FlutterView實現(xiàn)了BinaryMessenger骡尽,因此這里直接傳入了Flutter.createView()方法的返回值遣妥;第二個參數(shù)是name,就是Channel名稱攀细;第三個參數(shù)是codec箫踩,類型是MethodCodec,代表消息的編解碼器谭贪,這里沒有傳該參數(shù)境钟,默認使用StandardMethodCodec。
這里補充一下俭识,如果采用FlutterFragment的方式該如何獲取到FlutterView呢慨削,我們可以查看一下FlutterFragment的源碼。

public class FlutterFragment extends Fragment {
  public static final String ARG_ROUTE = "route";
  private String mRoute = "/";

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
      mRoute = getArguments().getString(ARG_ROUTE);
    }
  }

  @Override
  public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
    super.onInflate(context, attrs, savedInstanceState);
  }

  @Override
  public FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return Flutter.createView(getActivity(), getLifecycle(), mRoute);
  }
}

可以看到FlutterFragment的onCreateView()方法也是通過Flutter.createView()創(chuàng)建了FlutterView并返回套媚,因此可以通過Fragment的getView()方法獲取到FlutterView理盆。但是這里還有一個問題,在Activity中通過Flutter.createFragment()創(chuàng)建出Fragment后再調(diào)用getView()方法獲取到的View為null凑阶,這是因為只有在onCreateView()方法執(zhí)行完成后才會給Fragment持有的View賦值猿规,關(guān)于這個問題,我也沒有太好的解決方案宙橱,能想到的只是仿照FlutterFragment自定義一個Fragment姨俩,在內(nèi)部創(chuàng)建MethodChannel。

public class MyFlutterFragment extends FlutterFragment {

    private static final String CHANNEL_NATIVE = "com.example.flutter/native";

    public static MyFlutterFragment newInstance(String route) {
        MyFlutterFragment fragment = new MyFlutterFragment();
        Bundle args = new Bundle();
        args.putString(ARG_ROUTE, route);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 這里保證了getView()返回值不為null
        MethodChannel nativeChannel = new MethodChannel((FlutterView) getView(), CHANNEL_NATIVE);
        nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
                switch (methodCall.method) {
                    case "jumpToNative":
                        // 跳轉(zhuǎn)原生頁面
                        Intent jumpToNativeIntent = new Intent(getActivity(), NativePageActivity.class);
                        jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
                        startActivity(jumpToNativeIntent);
                        break;
                    default:
                        result.notImplemented();
                        break;
                }
            }
        });
    }
}

創(chuàng)建FlutterFragment時使用MyFlutterFragment.newInstance()代替Flutter.createFragment()师郑,傳入路由名稱和參數(shù)环葵。

FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
MyFlutterFragment flutterFragment = MyFlutterFragment.newInstance("route1?{\"name\":\"StephenCurry\"}");
tx.replace(R.id.fl_container, flutterFragment);
tx.commit();

這樣就解決了FlutterFragment獲取FlutterView的問題,不過我覺得這種方案并不好宝冕,將MethodChannel定義在了Fragment中张遭,耦合度太高,如果大家有更好的解決方案歡迎提出地梨,目前來看我還是建議使用Flutter.createView()的方式來引入Flutter頁面菊卷。
回到正題缔恳,定義好了MethodChannel之后調(diào)用setMethodCallHandler()方法設置消息處理回調(diào),參數(shù)是MethodHandler類型洁闰,需要實現(xiàn)它的onMethodCall()方法歉甚。onMethodCall()方法有兩個參數(shù)methodCallresultmethodCall記錄了調(diào)用的方法信息扑眉,包括方法名和參數(shù)纸泄,result用于方法的返回值,可以通過result.success()方法返回信息給Flutter端腰素。之后根據(jù)方法名和參數(shù)來執(zhí)行原生的代碼就可以了聘裁,這里是跳轉(zhuǎn)到原生Activity。
2.Flutter端
在Flutter端同樣需要定義一個MethodChannel弓千,使用MethodChannel需要引入services.dart包咧虎,Channel名稱要和Android端定義的相同。

static const nativeChannel =
    const MethodChannel('com.example.flutter/native');

在Flutter頁面中添加一個按鈕计呈,點擊按鈕執(zhí)行跳轉(zhuǎn)原生頁面操作砰诵,通過調(diào)用MethodChannel的invokeMethod()方法可以執(zhí)行原生代碼,該方法有兩個參數(shù)捌显,第一個是方法名茁彭,在Android端可以通過回調(diào)方法中的methodCall.method獲取到;第二個是方法的參數(shù)扶歪,可以不傳理肺,在Android端可以通過methodCall.arguments()以及methodCall.argument()獲取到所有參數(shù)或者指定名稱的參數(shù)。

RaisedButton(
    child: Text('跳轉(zhuǎn)Android原生頁面'),
    onPressed: () {
      // 跳轉(zhuǎn)原生頁面
      Map<String, dynamic> result = {'name': 'KlayThompson'};
      nativeChannel.invokeMethod('jumpToNative', result);
    })

這里我們也注意到了善镰,F(xiàn)lutter頁面跳轉(zhuǎn)原生頁面?zhèn)鬟f參數(shù)是通過invokeMethod()方法的第二個參數(shù)實現(xiàn)的妹萨,在Android端通過methodCall.argument()方法獲取到參數(shù)后再put到Intent里面就可以了。運行效果如下圖所示:

Flutter頁面跳轉(zhuǎn)Android原生頁面

到這里我們已經(jīng)基本實現(xiàn)了Flutter和Android原生之間的頁面跳轉(zhuǎn)和參數(shù)傳遞炫欺,此外還有一些需要我們注意的地方乎完。

  • 1.onActivityResult如何實現(xiàn)

在開發(fā)中我們經(jīng)常會遇到關(guān)閉當前頁面的同時返回給上一個頁面數(shù)據(jù)的場景,在Android中是通過startActivityForResultonActivityResult()實現(xiàn)的品洛,而純Flutter頁面之間可以通過在Navigator.of(context).pop()方法中添加參數(shù)來實現(xiàn)树姨,那么對于Flutter頁面和Android原生頁面之間如何在返回上一頁時傳遞數(shù)據(jù)呢,通過MethodChannel就可以實現(xiàn)桥状。
Flutter頁面返回Android原生頁面
這種情況直接在Flutter端調(diào)用原生的返回方法就可以了帽揪,首先在Flutter頁面添加一個按鈕,點擊按鈕返回原生頁面辅斟,代碼如下:

RaisedButton(
    child: Text('返回上一頁'),
    onPressed: () {
      // 返回給上一頁的數(shù)據(jù)
      Map<String, dynamic> result = {'message': '我從Flutter頁面回來了'};
      nativeChannel.invokeMethod('goBackWithResult', result);
    }),

Android端依然是通過判斷methodCall.method的值來執(zhí)行指定的代碼转晰,通過methodCall.argument()獲取Flutter傳遞的參數(shù)。

nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
        switch (methodCall.method) {
            case "goBackWithResult":
                // 返回上一頁,攜帶數(shù)據(jù)
                Intent backIntent = new Intent();
                backIntent.putExtra("message", (String) methodCall.argument("message"));
                setResult(RESULT_OK, backIntent);
                finish();
                break;
        }
    }
});

之后在上一個Activity的onActivityResult()方法中編寫邏輯就可以了查邢,這里就不展示了蔗崎。
Android原生頁面返回Flutter頁面
與上一種情況不同的是,這種情況需要原生來調(diào)用Flutter代碼侠坎,和Flutter調(diào)用原生方法的步驟是一樣的蚁趁,我們來具體看一下裙盾。首先在Flutter跳轉(zhuǎn)到的頁面NativePageActivity中添加一個按鈕实胸,點擊按鈕返回Flutter頁面,并傳遞數(shù)據(jù)番官。

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_native_page);

    Button btnBack = findViewById(R.id.btn_back);
    btnBack.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent();
            intent.putExtra("message", "我從原生頁面回來了");
            setResult(RESULT_OK, intent);
            finish();
        }
    });
}

然后修改一下Flutter跳轉(zhuǎn)原生頁面的代碼庐完,將startActivity改為startActivityForResult,并重寫onActivityResult()方法徘熔,在方法內(nèi)部獲取到原生頁面返回的數(shù)據(jù)门躯,創(chuàng)建MethodChannel,調(diào)用invokeMethod()方法將數(shù)據(jù)傳遞給Flutter端酷师,這里定義的方法名為"onActivityResult"讶凉。

private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case 0:
            if (data != null) {
                // NativePageActivity返回的數(shù)據(jù)
                String message = data.getStringExtra("message");
                Map<String, Object> result = new HashMap<>();
                result.put("message", message);
                // 創(chuàng)建MethodChannel,這里的flutterView即Flutter.createView所返回的View
                MethodChannel flutterChannel = new MethodChannel(flutterView, CHANNEL_FLUTTER);
                // 調(diào)用Flutter端定義的方法
                flutterChannel.invokeMethod("onActivityResult", result);
            }
            break;
        default:
            break;
    }
}

接下來需要在Flutter端定義MethodChannel和回調(diào)方法山孔,同樣是根據(jù)MethodCall.method的值來執(zhí)行相應代碼懂讯,通過MethodCall.arguments來獲取參數(shù)。

static const flutterChannel =
    const MethodChannel('com.example.flutter/flutter');

@override
void initState() {
  super.initState();
  Future<dynamic> handler(MethodCall call) async {
    switch (call.method) {
      case 'onActivityResult':
        // 獲取原生頁面?zhèn)鬟f的參數(shù)
        print(call.arguments['message']);
        break;
    }
  }

  flutterChannel.setMethodCallHandler(handler);
}

這樣就實現(xiàn)了原生頁面返回Flutter頁面并返回數(shù)據(jù)的場景台颠,獲取到數(shù)據(jù)后就可以為所欲為啦褐望。

  • 2.Flutter棧管理

看到這里不知道大家是否和我有相同的感受,在原生頁面(Activity)中引入Flutter頁面有些類似于Android開發(fā)中使用WebView加載url串前,每個Flutter頁面對應著一個route(url)瘫里,那么我們自然就會想到一個問題:如果在Flutter頁面中繼續(xù)跳轉(zhuǎn)到其他Flutter頁面,這時候點擊手機的返回鍵是否會直接返回到上一個Activity荡碾,而不是返回上一個Flutter頁面呢谨读,通過測試發(fā)現(xiàn)確實是這樣。

那么應該如何解決這個問題呢坛吁,我的實現(xiàn)思路是在Flutter端利用Navigator.canPop(context)方法判斷是否可以返回上一頁漆腌,如果可以就調(diào)用Navigator.of(context).pop()返回,反之則說明當前顯示的Flutter頁面已經(jīng)是第一個頁面了阶冈,直接返回上一個Activity即可闷尿。至于如何返回上一個Activity,當然還是要使用MethodChannel了女坑。既然明確了思路填具,我們就來看看具體實現(xiàn)吧。
首先在Flutter頁面中添加一個按鈕,點擊按鈕跳轉(zhuǎn)到一個新的Flutter頁面劳景,這里的SecondPage是我新建的一個頁面誉简,可以隨意修改,重點不在頁面本身盟广,就不展示出來了闷串。

RaisedButton(
    child: Text('跳轉(zhuǎn)Flutter頁面'),
    onPressed: () {
      Navigator.of(context)
          .push(MaterialPageRoute(builder: (context) {
        return SecondPage();
      }));
    }),

然后定義MethodChannel和MethodCallHandler回調(diào),這里的邏輯是是調(diào)用Navigator.canPop(context)判斷是否可以返回上一頁筋量,如果可以就調(diào)用Flutter自身的返回上一頁方法烹吵,如果已經(jīng)是第一個Flutter頁面了就調(diào)用原生方法返回上一個Activity,即這里的nativeChannel.invokeMethod('goBack')桨武。

static const nativeChannel =
    const MethodChannel('com.example.flutter/native');
static const flutterChannel =
    const MethodChannel('com.example.flutter/flutter');

@override
void initState() {
  super.initState();
  Future<dynamic> handler(MethodCall call) async {
    switch (call.method) {
      case 'goBack':
        // 返回上一頁
        if (Navigator.canPop(context)) {
          Navigator.of(context).pop();
        } else {
          nativeChannel.invokeMethod('goBack');
        }
        break;
    }
  }

  flutterChannel.setMethodCallHandler(handler);
}

接下來我們再來看Android端肋拔,首先需要重寫onBackPressed()方法,將返回鍵的事件處理交給Flutter端呀酸。

private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";

@Override
public void onBackPressed() {
    MethodChannel flutterChannel = new MethodChannel(flutterView, CHANNEL_FLUTTER);
    flutterChannel.invokeMethod("goBack", null);
}

最后編寫原生端的MethodCallHandler回調(diào)凉蜂,如果當前Flutter頁面是第一個時調(diào)用該方法直接finish掉Activity。

nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
    case "goBack":
        // 返回上一頁
        finish();
        break;
    default:
        result.notImplemented();
        break;
}

現(xiàn)在我們再來看看運行效果性誉,這樣就很舒服了窿吩。

Flutter升級到1.12后遇到的問題

前些日子評論區(qū)里wangwhatlh同學反饋
遇到了程序包io.flutter.facade不存在問題,起初我運行了一下之前的項目错览,發(fā)現(xiàn)可以正常運行纫雁,加上我自己有一段時間沒有用過Flutter了,也就沒太重視這個問題蝗砾。說來也是慚愧先较,最近又陸續(xù)有多位小伙伴反饋了這個問題,我才終于意識到這是一個普遍性問題悼粮,簡單查了一下了解到這個錯誤是Flutter 1.12版本廢棄了io.flutter.facade包導致的闲勺,我自己更新了Flutter版本后重新運行項目也遇到了這個問題,所以要對此前遇到這個問題的大家說聲抱歉扣猫,確實是我沒有重視這個問題菜循,之后對于大家提出的問題我一定盡快反饋。
好了申尤,接下來就介紹一下解決方案吧癌幕,首先附上官方的一些相關(guān)說明文檔,大家可以自行閱讀一下文檔昧穿,文檔中介紹的還是比較詳細的勺远。
Upgrading pre 1.12 Android projects
Experimental: Add Flutter View
Add Flutter to existing app
下面進入正題,簡單介紹一下在Flutter 1.12版本中幾個需要修改的地方时鸵。

  • 原生頁面中引入Flutter

上文在介紹Android原生頁面跳轉(zhuǎn)Flutter頁面時提到了兩種方案:FlutterView和FlutterFragment胶逢,我們來分別看一下現(xiàn)在應該如何實現(xiàn)厅瞎。
首先是通過FlutterView引入Flutter頁面,以前我們是通過io.flutter.facade包中Flutter類的createView()方法創(chuàng)建出一個FlutterView初坠,然后添加到Activity的布局中和簸,但是由于io.flutter.facade包的廢棄,該方法已經(jīng)無法使用碟刺。官方的文檔有說明目前不提供在View級別引入Flutter的便捷API锁保,因此如果可能的話,我們應該避免使用FlutterView半沽,但是通過FlutterView引入Flutter頁面也是可行的爽柒,代碼如下:

// 通過FlutterView引入Flutter編寫的頁面
FlutterView flutterView = new FlutterView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout flContainer = findViewById(R.id.fl_container);
flContainer.addView(flutterView, lp);
// 關(guān)鍵代碼,將Flutter頁面顯示到FlutterView中
flutterView.attachToFlutterEngine(flutterEngine);

需要注意抄囚,這里的FlutterView位于io.flutter.embedding.android包中霉赡,和此前我們所創(chuàng)建的FlutterView(位于io.flutter.view包中)是不一樣的橄务。我們通過查看FlutterView的源碼可以發(fā)現(xiàn)它繼承自FrameLayout幔托,因此像一個普通的View那樣添加就可以了。接下來的這一步很關(guān)鍵蜂挪,調(diào)用FlutterView的attachToFlutterEngine()方法重挑,這個方法的作用就是將Flutter編寫的UI頁面顯示到FlutterView中,我們注意到這里傳入了一個flutterEngine參數(shù)棠涮,它又是什么呢谬哀?flutterEngine的類型為FlutterEngine,字面意思就是Flutter引擎严肪,它負責在Android端執(zhí)行Dart代碼史煎,將Flutter編寫的UI顯示到FlutterView/FlutterActivity/FlutterFragment中。創(chuàng)建FlutterEngine的代碼如下:

FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getDartExecutor().executeDartEntrypoint(
        DartExecutor.DartEntrypoint.createDefault()
);

這樣就創(chuàng)建好了一個FlutterEngine對象驳糯,默認情況下FlutterEngine加載的路由名稱為"/"篇梭,我們可以通過下面的代碼指定初始路由名稱:

flutterEngine.getNavigationChannel().setInitialRoute("route1");

至于傳參的情況沒有變化,直接在路由名稱后面拼接參數(shù)就可以了酝枢。當然恬偷,F(xiàn)lutterView也可以直接在xml布局文件中添加,最后同樣需要調(diào)用attachToFlutterEngine()方法將Flutter編寫的UI頁面顯示到FlutterView中帘睦,這里就不展示了袍患。
補充一下,最近我將Flutter版本更新到了1.17竣付,發(fā)現(xiàn)上述代碼運行后FlutterView無法顯示诡延,和官方提供的示例flutter_view進行了對比,才發(fā)現(xiàn)缺少了下面的代碼:

@Override
protected void onResume() {
    super.onResume();
    flutterEngine.getLifecycleChannel().appIsResumed();
}

@Override
protected void onPause() {
    super.onPause();
    flutterEngine.getLifecycleChannel().appIsInactive();
}

@Override
protected void onStop() {
    super.onStop();
    flutterEngine.getLifecycleChannel().appIsPaused();
}

相信大家都能看出這和生命周期有關(guān)古胆,flutterEngine.getLifecycleChannel()獲取到的是一個LifecycleChannel對象肆良,類比于MethodChannel,作用大概就是將Flutter和原生端的生命周期相互聯(lián)系起來。這里分別在onResume()妖滔、onPause()onStop()方法中調(diào)用了LifecycleChannel的appIsResumed()隧哮、appIsInactive()appIsPaused()方法,作用就是同步Flutter端與原生端的生命周期座舍。添加上述代碼后沮翔,F(xiàn)lutterView就可以正常顯示了。至于為什么在Flutter 1.17版本(也有可能是更早的版本)中需要添加上述代碼曲秉,我猜想可能是FlutterVIew的渲染機制有了一些變化采蚀,在接收到原生端對應生命周期方法中發(fā)送的通知才會顯示,具體原理我也不是很清楚承二,如果有說得不對的地方或是大家有了解這部分內(nèi)容的歡迎提出榆鼠。
然后是通過FlutterFragment引入Flutter頁面,我們此前是通過Flutter.createFragment()方法創(chuàng)建出FlutterFragment亥鸠,現(xiàn)在同樣無法使用了妆够。官方提供了三種創(chuàng)建FlutterFragment的方式,我們來分別看一下负蚊。
方式一神妹、FlutterFragment.createDefault()

// 通過FlutterFragment引入Flutter編寫的頁面
FlutterFragment flutterFragment = FlutterFragment.createDefault();
getSupportFragmentManager()
        .beginTransaction()
        .add(R.id.fl_container, flutterFragment)
        .commit();

通過FlutterFragment.createDefault()創(chuàng)建出FlutterFragment,需要注意這里的FlutterFragment位于io.flutter.embedding.android包中家妆,和我們此前使用的FlutterFragment不是同一個類鸵荠。創(chuàng)建好之后就沒什么可說的了,按照正常的Fragment添加就好伤极。createDefault()方法創(chuàng)建出的Fragment顯示的路由名稱為"/"蛹找,如果我們需要指定其他路由名稱就不能使用這個方法了。
方式二哨坪、FlutterFragment.withNewEngine()

// 通過FlutterFragment引入Flutter編寫的頁面
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
        .initialRoute("route1")
        .build();
getSupportFragmentManager()
        .beginTransaction()
        .add(R.id.fl_container, flutterFragment)
        .commit();

通過FlutterFragment.withNewEngine()獲取到NewEngineFragmentBuilder對象庸疾,使用建造者模式構(gòu)造出FlutterFragment對象,可以通過initialRoute()方法指定初始路由名稱齿税。同樣地彼硫,傳遞參數(shù)只需要在路由名稱后面進行拼接。
方式三凌箕、FlutterFragment.withCachedEngine

// 創(chuàng)建可緩存的FlutterEngine對象
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute("route1");
flutterEngine.getDartExecutor().executeDartEntrypoint(
        DartExecutor.DartEntrypoint.createDefault()
);
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);

// 通過FlutterFragment引入Flutter編寫的頁面
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
        .build();

方式二使用的withNewEngine()方法從名稱上也能看出每次都是創(chuàng)建一個新的FlutterEngine對象來顯示Flutter UI拧篮,但是從官方文檔中我們可以了解到每個FlutterEngine對象在顯示出Flutter UI之前是需要一個warm-up(不知道能不能翻譯為預熱)期的,這會導致屏幕呈現(xiàn)短暫的空白牵舱,解決方式就是預先創(chuàng)建并啟動FlutterEngine串绩,完成warm-up過程,然后將這個FlutterEngine緩存起來芜壁,之后使用這個FlutterEngine來顯示出Flutter UI礁凡。上面的代碼中執(zhí)行的FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)就是將FlutterEngine緩存起來高氮,這里傳入的"my_engine_id"就相當于緩存名稱。之后通過FlutterFragment.withCachedEngine()方法來創(chuàng)建FlutterFragment顷牌,參數(shù)傳入上面的緩存名稱剪芍。需要注意,withCachedEngine()方法返回的是一個CachedEngineFragmentBuilder對象窟蓝,同樣是使用了建造者模式罪裹,但是它是沒有initialRoute()方法的,如果我們要指定初始路由运挫,需要在創(chuàng)建FlutterEngine對象時通過setInitialRoute()方法來設置状共。
除此之外,F(xiàn)lutter 1.12中還提供了一種原生引入Flutter頁面方式——使用FlutterActivity谁帕,這里的FlutterActivity也是位于io.flutter.embedding.android包下的峡继。下面我簡單介紹一下如何通過FlutterActivity引入Flutter編寫的UI,大家也可以參考官網(wǎng)的介紹匈挖。
首先需要在AndroidManifest.xml文件中注冊FlutterActivity碾牌,代碼如下:

<activity
    android:name="io.flutter.embedding.android.FlutterActivity"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    android:hardwareAccelerated="true"
    android:theme="@style/AppTheme"
    android:windowSoftInputMode="adjustResize" />

這里的theme可以替換為自己項目中定義的主題。注冊好FlutterActivity后关划,第二步就是直接啟動這個Activity了小染,啟動FlutterActivity有以下三種方式:

// 方式一翘瓮、FutterActivity顯示的路由名稱為"/"贮折,不可設置
startActivity(
        FlutterActivity.createDefaultIntent(this)
);

// 方式二、FutterActivity顯示的路由名稱可設置资盅,每次都創(chuàng)建一個新的FlutterEngine對象
startActivity(
        FlutterActivity
                .withNewEngine()
                .initialRoute("route1")
                .build(this)
);

// 方式三调榄、FutterActivity顯示的路由名稱可設置,使用緩存好的FlutterEngine對象
startActivity(
        FlutterActivity
                .withCachedEngine("my_engine_id")
                .build(this)
);

是不是很熟悉呵扛,和上面介紹的創(chuàng)建FlutterFragment的三種方式是對應的每庆,這里我就不再介紹了。與通過FlutterView/FlutterFragment引入Flutter UI不同今穿,這種方式不需要我們自己創(chuàng)建一個Activity缤灵,F(xiàn)lutterActivity顯示的Flutter路由是在創(chuàng)建Intent對象時指定的,優(yōu)點就是使用起來更簡單蓝晒,缺點就是不夠靈活腮出,無法像FlutterView/FlutterFragment那樣只是作為原生頁面中的一部分展示,因此這種方式更適合整個頁面都是由Flutter編寫的場景芝薇。
在調(diào)試階段胚嘲,跳轉(zhuǎn)FlutterActivity之后也會黑屏幾秒,同樣地洛二,打了release包后就沒有問題了馋劈。

  • Android原生與Flutter交互

交互這塊的變化不大攻锰,還是使用MethodChannel來進行方法的調(diào)用,但是在Android端創(chuàng)建MethodChannel時需要注意了妓雾,我們此前都是傳入io.flutter.view包下的FlutterView作為BinaryMessenger娶吞,現(xiàn)在肯定是無法獲取到該類對象了,那么這個參數(shù)應該傳什么呢械姻。通過查看繼承關(guān)系我們可以找到兩個相關(guān)的類:DartExecutorDartMessenger寝志。DartExecutor可以通過FlutterEngine的getDartExecutor()方法獲得,而DartMessenger又可以通過DartExecutor的getBinaryMessenger()方法獲得策添,因此我們可以這樣創(chuàng)建MethodChannel:

MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), "com.example.flutter/native");
// 或
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.flutter/native");

至于這兩種方式使用哪個材部,我目前是沒發(fā)現(xiàn)有什么表現(xiàn)上的區(qū)別,如果哪位大佬有了解過可以分享一下唯竹。
此外乐导,我們還需要注意,這里在創(chuàng)建MethodChannel時傳入的FlutterEngine對象必須和我們此前創(chuàng)建好的FlutterView/FlutterFragment中使用的是同一個浸颓。拿FlutterFragment舉例物臂,上面介紹了三種創(chuàng)建FlutterFragment的方式,第三種是使用已經(jīng)創(chuàng)建好的FlutterEngine對象产上,我們可以直接傳入這個FlutterEngine對象棵磷。但是前兩種方式都是會創(chuàng)建出一個新的FlutterEngine,我們?nèi)绾潍@取到FlutterEngine對象呢晋涣?FlutterFragment中定義了一個getFlutterEngine()方法仪媒,從方法名來看大概就是獲取FlutterEngine對象。我嘗試過創(chuàng)建MethodChannel時傳入flutterFragment.getFlutterEngine().getDartExecutor()谢鹊,運行后會直接拋出空指針異常算吩,異常產(chǎn)生的位置在FlutterFragment的getFlutterEngine()方法中:

@Nullable
public FlutterEngine getFlutterEngine() {
    return delegate.getFlutterEngine();
}

錯誤原因是這里的delegate為null,全局搜索一下佃扼,發(fā)現(xiàn)在FlutterFragment的onAttach()方法中會對delegate賦值偎巢,也就是說明此時沒有執(zhí)行onAttach()方法。我猜測這就是由于上面提到過的FlutterEngine的warm-up機制兼耀,這是一個耗時過程压昼,因此FlutterFragment并不會立刻執(zhí)行onAttach()方法,導致我們在Activity的onCreate()方法中直接使用FlutterFragment的getFlutterEngine()方法會拋出異常瘤运。目前我也沒想到有什么解決方案窍霞,如果我們要利用FlutterFragment來進行交互,還是只能使用withCachedEngine()方法來創(chuàng)建FlutterFragment尽超,在構(gòu)造MethodChannel時傳入創(chuàng)建好的FlutterEngine對象官撼。
到這里Flutter 1.12中關(guān)于原生交互的幾個變更基本上就介紹得差不多了,相關(guān)代碼我也已經(jīng)更新似谁。最后還是要感嘆一下Flutter的更新速度傲绣,才幾個月沒看就變化這么大掠哥,之后的版本可能還會修改,如果我了解到有什么變更會及時更新文章秃诵,大家有什么發(fā)現(xiàn)也歡迎提出续搀。

關(guān)于AndroidX

現(xiàn)在越來越多的Android項目都使用了AndroidX庫,之前寫這篇文章時由于還是使用的support庫菠净,因此這里簡單介紹一下AndroidX的遷移吓懈。
關(guān)于Android原生項目的遷移我就不介紹了歼培,相信大家接觸過Android開發(fā)的都有了解過行贪,網(wǎng)上也有很多相關(guān)文章芬为。在Flutter版本1.12.13之后,新建的Module默認就是使用AndroidX庫的攀唯,那么如何把現(xiàn)有的Flutter Module遷移到AndroidX呢洁桌,其實很簡單,在Flutter Module根目錄下的pubspec.yaml文件中添加下面的配置就可以了:

module:
  androidX: true // Add this line.

添加之后在命令行執(zhí)行flutter clean命令即可侯嘀,執(zhí)行完成后我們打開.android(或android)目錄下的gradle.properties文件另凌,會看到添加了如下配置,這就說明Flutter Module已經(jīng)成功遷移到了AndroidX庫戒幔。

當我們把Flutter Module和Android項目都遷移到AndroidX庫之后會發(fā)現(xiàn)代碼中有報錯:

可以看到是在調(diào)用FragmentTransaction的add()方法時報的錯吠谢,報錯的原因就是因為這里傳入的參數(shù)類型(flutterFragment類型)不正確,我們可以點開FlutterFragment類看一下诗茎,發(fā)現(xiàn)在類文件中定義的FlutterFragment類還是繼承自support包下的Fragment工坊,那么這里當然就會報錯了。先別慌错沃,當你嘗試運行項目時會發(fā)現(xiàn)可以正常運行栅组,這是為什么呢?其實一開始我也有寫疑惑枢析,后來仔細想了想大概是android.enableJetifier=true這個配置的作用,上面也見到過刃麸,是項目遷移到AndroidX庫后自動添加的醒叁,大家可能知道它的作用是將項目中的第三方庫自動遷移到AndroidX庫,因此雖然我們從源碼中看到的FlutterFragment類還是使用的support包泊业,但是gardle在編譯時已經(jīng)自動將FlutterFragment類遷移到了AndroidX庫把沼,所以運行時不會報錯。
關(guān)于AndroidX庫遷移的具體內(nèi)容大家也可以查看官方文檔AndroidX Migration吁伺。
目前最新版本(1.18.0)的Flutter中饮睬,F(xiàn)lutterFragment已經(jīng)改為繼承自androidx包下的Fragment了,編輯器也不會報錯了篮奄,不過具體是哪個版本修改的我就不清楚了捆愁,如果大家有知道的歡迎提出割去。

總結(jié)

本文介紹了Android項目中引入Flutter的方法以及簡單交互場景的實現(xiàn)。
1.Android項目引入Flutter本質(zhì)上是將Flutter編寫的Widget嵌入到Activity中昼丑,類似于WebView呻逆,容器Activity相當于WebView,route相當于url菩帝,有兩種方式FlutterView和FlutterFragment咖城。頁面間的跳轉(zhuǎn)和傳參可以借助MethodChannel來實現(xiàn)。
2.關(guān)于MethodChannel呼奢,它的作用是Flutter和原生方法的互相調(diào)用宜雀,使用時在兩端都要定義MethodChannel,通過相同的name聯(lián)系起來握础,調(diào)用方使用invokeMethod()州袒,傳入方法名和參數(shù);被調(diào)用方定義MethodCallHandler回調(diào)弓候,根據(jù)方法名和方法參數(shù)執(zhí)行相應的平臺代碼郎哭。
3.本文中所提到的一些方案可能并不是最好的,如果大家有自己的見解歡迎一起交流學習菇存。此外夸研,文中的一些代碼只展示了部分,Demo我已經(jīng)上傳到了github依鸥,大家如果需要的話可以查看亥至。
4.最后提一下flutter_boost,這是閑魚團隊開源的一個Flutter混合開發(fā)插件贱迟,我簡單地嘗試了一下姐扮,還是挺好用的,在頁面跳轉(zhuǎn)和傳參方面都很方便衣吠,大家感興趣的話可以了解一下茶敏。

參考文章

Add Flutter to existing app
Flutter混編:在Android原生中混編Flutter
flutter接入現(xiàn)有的app詳細介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缚俏,隨后出現(xiàn)的幾起案子惊搏,更是在濱河造成了極大的恐慌,老刑警劉巖忧换,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恬惯,死亡現(xiàn)場離奇詭異,居然都是意外死亡亚茬,警方通過查閱死者的電腦和手機酪耳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刹缝,“玉大人碗暗,你說我怎么就攤上這事颈将。” “怎么了讹堤?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵吆鹤,是天一觀的道長。 經(jīng)常有香客問我洲守,道長疑务,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任梗醇,我火速辦了婚禮知允,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘叙谨。我一直安慰自己温鸽,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布手负。 她就那樣靜靜地躺著涤垫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪竟终。 梳的紋絲不亂的頭發(fā)上蝠猬,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音统捶,去河邊找鬼榆芦。 笑死,一個胖子當著我的面吹牛喘鸟,可吹牛的內(nèi)容都是我干的匆绣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼什黑,長吁一口氣:“原來是場噩夢啊……” “哼崎淳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起兑凿,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤凯力,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后礼华,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡拗秘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年圣絮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雕旨。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡扮匠,死狀恐怖捧请,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情棒搜,我是刑警寧澤疹蛉,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站力麸,受9級特大地震影響可款,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜克蚂,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一闺鲸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧埃叭,春花似錦摸恍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至类早,卻和暖如春媚媒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背莺奔。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工欣范, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人令哟。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓恼琼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屏富。 傳聞我的和親對象是個殘疾皇子晴竞,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容