Android 車載應(yīng)用開發(fā)與分析 (3)- 構(gòu)建 MVVM 架構(gòu)(Java版)

前言

在大多數(shù)車載系統(tǒng)應(yīng)用架構(gòu)中莽鸿,一個完整的應(yīng)用往往會包含三層:

  • HMI
    Human Machine Interface,顯示UI信息,進(jìn)行人機(jī)交互。

  • Service
    在系統(tǒng)后臺進(jìn)行數(shù)據(jù)處理扮惦,監(jiān)控數(shù)據(jù)狀態(tài)。

  • SDK
    根據(jù)業(yè)務(wù)邏輯Service對外暴露的通信接口亲桦,其他模塊通過它來完成IPC通信崖蜜。

當(dāng)然并不是所有的應(yīng)用都需要Service,只有不能長久的駐留在內(nèi)存中客峭,且需要監(jiān)控系統(tǒng)數(shù)據(jù)和行為的應(yīng)用才需要Service豫领。

舉個例子,系統(tǒng)的OTA需要一個Service在IVI的后臺監(jiān)控云服務(wù)或SOA接口的消息舔琅,然后完成升級包的下載等等恐。也需要一個HMI顯示升級的Release Note、確認(rèn)用戶是否同意升級等搏明,這個HMI往往會被歸納在系統(tǒng)設(shè)置中鼠锈。ServiceHMI之間的IPC通信闪檬,則需要暴露一個SDK來完成星著,這個其他模塊的HMI也可以通過這個SDK完成與Service的IPC通信。

反例則是粗悯,Launcher 可以長久的駐留在內(nèi)存虚循,所以它也就不需要ServiceSDK

本篇文章主要講解,如在HMI層中構(gòu)建一個適合車載系統(tǒng)應(yīng)用的MVVM架構(gòu)横缔。本文涉及的源碼:https://github.com/linux-link/CarMvvmArch

MVVM 架構(gòu)分層邏輯

MVVM 架構(gòu)的原理以及與MVC&MVP的區(qū)別铺遂,網(wǎng)上已經(jīng)有很多相關(guān)的優(yōu)秀文章,這里就不再贅述茎刚,本篇文章將聚焦如何車載應(yīng)用中利用Jetpack組件將 MVVM 架構(gòu)真正落地實(shí)現(xiàn)襟锐。

圖-1 MVVM架構(gòu)圖

當(dāng)前的Android應(yīng)用的MVVM架構(gòu)分層邏輯,都源自圖-2 Android官方給出的指導(dǎo)建議膛锭,我們也同樣基于這套邏輯來實(shí)現(xiàn)MVVM架構(gòu)粮坞。

圖-2 Android官方推薦的APP架構(gòu)設(shè)計圖

封裝適合車載應(yīng)用 MVVM 框架

車載應(yīng)用相對于手機(jī)應(yīng)用來說開發(fā)周期和復(fù)雜度都要小很多,所以我們封裝的重點(diǎn)是View層初狰,ViewModel 層和 Model 層的封裝則會相對簡單一些莫杈。

封裝 Model 層

一般來說我們會把訪問網(wǎng)絡(luò)的工具類封裝在Model層,但是車載系統(tǒng)應(yīng)用的 HMI 層通常沒有訪問網(wǎng)絡(luò)的功能奢入,所以 Model 層我們直接留空即可筝闹。

public abstract class BaseRepository {

}

封裝 ViewModel 層

VideModel 層的封裝很簡單,只需要將Model的實(shí)例傳入腥光,方便 ViewModel 的實(shí)現(xiàn)類調(diào)用即可关顷。

封裝 ViewModel

public abstract class BaseViewModel<M extends BaseRepository> extends ViewModel {

    protected M mRepository;

    public BaseViewModel(M repository) {
        mRepository = repository;
    }

    public M getRepository() {
        return mRepository;
    }
}

封裝 AndroidViewModel

public abstract class BaseAndroidViewModel<M extends BaseRepository> extends AndroidViewModel {

    protected M mRepository;

    public BaseAndroidViewModel(Application application, @Nullable M repository) {
        super(application);
        mRepository = repository;
    }

    public M getRepository() {
        return mRepository;
    }
}

封裝 View 層

在 View 層中我們需要引入DatabindingViewModel,并且定義出 View 的一些實(shí)現(xiàn)規(guī)范武福。

在實(shí)際使用中解寝,并不是每一個界面都需要使用MVVM架構(gòu), 所以需要額外封裝一個只引入DatabindingFrangmentActivity

基于 DataBinding 封裝 Fragment

public abstract class BaseBindingFragment<V extends ViewDataBinding> extends BaseFragment {

    private static final String TAG = TAG_FWK + BaseBindingFragment.class.getSimpleName();

    protected V mBinding;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        LogUtils.logV(TAG, "[onCreateView]");
        if (getLayoutId() == 0) {
            throw new RuntimeException("getLayout() must be not null");
        }
        mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
        mBinding.setLifecycleOwner(this);
        mBinding.executePendingBindings();
        initView();
        return mBinding.getRoot();
    }

    protected abstract void initView();

    @LayoutRes
    protected abstract int getLayoutId();

    public V getBinding() {
        return mBinding;
    }
}

BindingFragment 的基礎(chǔ)上添加 ViewModel

public abstract class BaseMvvmFragment<Vm extends BaseViewModel, V extends ViewDataBinding> extends BaseBindingFragment<V> {

    protected Vm mViewModel;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        initViewModel();
        View view = super.onCreateView(inflater, container, savedInstanceState);
        initObservable(mViewModel);
        if (getViewModelVariable() != 0) {
            mBinding.setVariable(getViewModelVariable(), mViewModel);
        }
        return view;
    }

    @Override
    public void onStart() {
        super.onStart();
        loadData(getViewModel());
    }

    private void initViewModel() {
        Class<Vm> modelClass;
        Type type = getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            modelClass = (Class<Vm>) ((ParameterizedType) type).getActualTypeArguments()[0];
        } else {
            modelClass = (Class<Vm>) BaseViewModel.class;
        }
        Object  object = getViewModelOrFactory();
        if (object instanceof ViewModel){
            mViewModel = (Vm) object;
        }else if (object instanceof ViewModelProvider.Factory){
            mViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) object)
                    .get(modelClass);
        }else {
            mViewModel = new ViewModelProvider(this,
                    new ViewModelProvider.NewInstanceFactory()).get(modelClass);
        }
    }

    protected abstract Object getViewModelOrFactory();

    protected abstract int getViewModelVariable();

    protected abstract void initObservable(Vm viewModel);

    protected abstract void loadData(Vm viewModel);

    protected Vm getViewModel() {
        return mViewModel;
    }
}

基于 DataBinding 封裝 Activity

public abstract class BaseBindingActivity<V extends ViewDataBinding> extends BaseActivity {

    protected V mBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getLayoutId() == 0) {
            throw new RuntimeException("getLayout() must be not null");
        }
        mBinding = DataBindingUtil.setContentView(this, getLayoutId());
        mBinding.setLifecycleOwner(this);
        mBinding.executePendingBindings();
        initView();
    }

    @LayoutRes
    protected abstract int getLayoutId();

    public V getBinding() {
        return mBinding;
    }

    protected abstract void initView();
}

在 BindingActivity 的基礎(chǔ)上添加 ViewModel

public abstract class BaseMvvmActivity<Vm extends BaseViewModel, V extends ViewDataBinding> extends BaseBindingActivity<V> {

    protected Vm mViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        initViewModel();
        super.onCreate(savedInstanceState);
        if (getViewModelVariable() != 0) {
            mBinding.setVariable(getViewModelVariable(), mViewModel);
        }
        mBinding.executePendingBindings();
        initObservable(mViewModel);
    }

    @Override
    protected void onStart() {
        super.onStart();
        loadData(mViewModel);
    }

    private void initViewModel() {
        Class<Vm> modelClass;
        Type type = getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            modelClass = (Class<Vm>) ((ParameterizedType) type).getActualTypeArguments()[0];
        } else {
            modelClass = (Class<Vm>) BaseViewModel.class;
        }
        Object  object = getViewModelOrFactory();
        if (object instanceof BaseViewModel){
            mViewModel = (Vm) object;
        }else if (object instanceof ViewModelProvider.Factory){
            mViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) object)
                    .get(modelClass);
        }else {
            mViewModel = new ViewModelProvider(this,
                    new ViewModelProvider.NewInstanceFactory()).get(modelClass);
        }
    }

    protected abstract Object getViewModelOrFactory();

    protected abstract int getViewModelVariable();

    protected abstract void initObservable(Vm viewModel);

    protected abstract void loadData(Vm viewModel);

    protected Vm getViewModel() {
        return mViewModel;
    }
}

重點(diǎn)解釋一下幾個abstract的方法

  • Object getViewModelOrFactory()

返回ViewModel的實(shí)例或ViewModelFactory實(shí)例

  • int getViewModelVariable()

返回XML中ViewModel的Variable****Id艘儒。例如:BR.viewModel.

  • void initObservable(Vm viewModel)

在此處操作ViewModel中LiveData的聋伦。例如:下面這類方法,都應(yīng)該寫在這個方法體里面界睁。目的是為了便于維護(hù)

viewModel.getTempLive().observe(this, new Observer<String>() {
    @Override
    public void onChanged(String temp) {
        LogUtils.logI(TAG, "[onChanged] " + temp);
    }
});

  • void initView()

在此處進(jìn)行初始化UI的操作觉增。例如:初始化RecyclerView,設(shè)定ClickListener等等翻斟。

  • void loadData(Vm viewModel)

在此處使用ViewModel進(jìn)行請求用于初始化UI的數(shù)據(jù)逾礁。

基于框架實(shí)現(xiàn)MVVM架構(gòu)

接下來我們基于上面封裝的 MVVM 框架,來實(shí)現(xiàn)一個最基礎(chǔ)的 MVVM 架構(gòu)下的demo访惜。

定義公共組件

創(chuàng)建 ViewModelFactory

定義ViewModel的實(shí)例化方式嘹履,單一Module下ViewModel的創(chuàng)建應(yīng)該集中在一個ViewModelFactory

// default 權(quán)限,不對外部公開此類
class AppViewModelFactory implements ViewModelProvider.Factory {

    // 創(chuàng)建 viewModel 實(shí)例
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        try {
            if (modelClass == HvacViewModel.class) {
                return modelClass.getConstructor(HvacRepository.class, AppExecutors.class)
                        .newInstance(AppInjection.getHvacRepository(), AppExecutors.get());
            } else {
                throw new RuntimeException(modelClass.getSimpleName() + "create failed");
            }
        } catch (NoSuchMethodException | IllegalAccessException
                | InstantiationException | InvocationTargetException exception) {
            exception.printStackTrace();
            throw new RuntimeException(exception);
        }
    }
}

創(chuàng)建 AppInjection

如果應(yīng)用中沒有使用 DaggerHilt 等依賴注入框架债热,那么為了便于日后的維護(hù)砾嫉,無論是車載應(yīng)用還是手機(jī)應(yīng)用,都建議定義一個AppInjection來將應(yīng)用中的單例窒篱、ViewModel焕刮、Repository等實(shí)例的獲取統(tǒng)一到一個入口程序中舶沿。

public class AppInjection {

    // ViewModel 工廠
    private final static AppViewModelFactory mViewModelFactory = new AppViewModelFactory();

    public static <T extends ViewModel> T getViewModel(ViewModelStoreOwner store, Class<T> clazz) {
        return new ViewModelProvider(store, mViewModelFactory).get(clazz);
    }

    public static AppViewModelFactory getViewModelFactory() {
        return mViewModelFactory;
    }

    /**
     * 受保護(hù)的權(quán)限,除了ViewModel,其它模塊不應(yīng)該需要Model層的實(shí)例
     *
     * @return {@link HvacRepository}
     */
    protected static HvacRepository getHvacRepository() {
        return new HvacRepository(getHvacManager());
    }

    public static HvacManager getHvacManager() {
        return HvacManager.getInstance();
    }

}

構(gòu)建 Model 層

在車載應(yīng)用中 Model 層的主要數(shù)據(jù)源無外乎 有三種網(wǎng)絡(luò)數(shù)據(jù)源配并、HMI本地數(shù)據(jù)源括荡、IPC(進(jìn)程間通信)數(shù)據(jù)源,其中最常見的是只有IPC數(shù)據(jù)源溉旋,三種數(shù)據(jù)源都有的情況往往會出現(xiàn)在主機(jī)廠商自行開發(fā)的車載地圖應(yīng)用中畸冲。所以我們這里只考慮如何基于IPC數(shù)據(jù)源構(gòu)造Model

定義一個 XXX``Repository 繼承自 BaseRepository,再根據(jù)業(yè)務(wù)需要定義出我們需要使用的接口观腊,這里的HvacManager就是service提供的用來進(jìn)行跨進(jìn)程通信的IPC-SDK中的入口召夹。

public class HvacRepository extends BaseRepository {

    private static final String TAG = IpcApp.TAG_HVAC + HvacRepository.class.getSimpleName();

    private final HvacManager mHvacManager;
    private HvacCallback mHvacViewModelCallback;

    private final IHvacCallback mHvacCallback = new IHvacCallback() {
        @Override
        public void onTemperatureChanged(double temp) {
            if (mHvacViewModelCallback != null) {
                // 處理遠(yuǎn)程數(shù)據(jù),講他轉(zhuǎn)換為應(yīng)用中需要的數(shù)據(jù)格式或內(nèi)容
                String value = String.valueOf(temp);
                mHvacViewModelCallback.onTemperatureChanged(value);
            }
        }
    };

    public HvacRepository(HvacManager hvacManager) {
        mHvacManager = hvacManager;
        mHvacManager.registerCallback(mHvacCallback);
    }

    public void clear() {
        mHvacManager.unregisterCallback(mHvacCallback);
    }

    public void requestTemperature() {
        LogUtils.logI(TAG, "[requestTemperature]");
        mHvacManager.requestTemperature();
    }

    public void setTemperature(int temperature) {
        LogUtils.logI(TAG, "[setTemperature] " + temperature);
        mHvacManager.setTemperature(temperature);
    }

    public void setHvacListener(HvacCallback callback) {
        LogUtils.logI(TAG, "[setHvacListener] " + callback);
        mHvacViewModelCallback = callback;
    }

    public void removeHvacListener(HvacCallback callback) {
        LogUtils.logI(TAG, "[removeHvacListener] " + callback);
        mHvacViewModelCallback = null;
    }

}

Repository通過一個HvacCallback將監(jiān)聽的遠(yuǎn)程數(shù)據(jù)處理后返回給ViewModel恕沫。

如果應(yīng)用會與多個不同的模塊進(jìn)行IPC通信监憎,那么建議將這些由不同模塊提供的IPC-SDK封裝在一個Manager中進(jìn)行統(tǒng)一管理。

構(gòu)建ViewModel

在Jetpack中ViewModel的用途是封裝界面控制器的數(shù)據(jù)婶溯,以使數(shù)據(jù)在配置更改后仍然存在鲸阔。在Android的MVVM 架構(gòu)設(shè)計中,ViewModel是最關(guān)鍵的一層迄委,通過持有Repository的引用來進(jìn)行外部通信

public class HvacViewModel extends BaseViewModel<HvacRepository> {

    private static final String TAG = IpcApp.TAG_HVAC + HvacViewModel.class.getSimpleName();

    private final HvacRepository mRepository;
    // 線程池框架褐筛。某些場景,ViewModel訪問Repository中的方法可能會需要切換到子線程叙身。
    private final AppExecutors mAppExecutors;
    private MutableLiveData<String> mTempLive;

    private final HvacCallback mHvacCallback = new HvacCallback() {
        @Override
        public void onTemperatureChanged(String temp) {
            LogUtils.logI(TAG, "[onTemperatureChanged] " + temp);
            getTempLive().postValue(temp);
        }
    };

    public HvacViewModel(HvacRepository repository, AppExecutors executors) {
        super(repository);
        mRepository = repository;
        mAppExecutors = executors;
        mRepository.setHvacListener(mHvacCallback);
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        mRepository.removeHvacListener(mHvacCallback);
        mRepository.release();
    }

    /**
     * 請求頁面數(shù)據(jù)
     */
    public void requestTemperature() {
        mRepository.requestTemperature();
    }

    /**
     * 將溫度數(shù)據(jù)設(shè)定到Service中
     *
     * @param view
     */
    public void setTemperature(View view) {
        mRepository.setTemperature(getTempLive().getValue());
    }

    public MutableLiveData<String> getTempLive() {
        if (mTempLive == null) {
            mTempLive = new MutableLiveData<>();
        }
        return mTempLive;
    }
}

構(gòu)建View層

最后就是構(gòu)建View層渔扎,一把就是Activity/Fragment和XML。

HvacActivity中各個方法含義我們上面封裝BaseMvvmActivity的時候已經(jīng)解釋過了信轿,這里不再贅述晃痴。

public class HvacActivity extends BaseMvvmActivity<HvacViewModel, ActivityHvacBinding> {

    private static final String TAG = IpcApp.TAG_HVAC + HvacActivity.class.getSimpleName();

    @Override
    protected int getLayoutId() {
        return R.layout.activity_hvac;
    }

    @Override
    protected Object getViewModelOrFactory() {
        return AppInjection.getViewModelFactory();
    }

    @Override
    protected int getViewModelVariable() {
        return BR.viewModel;
    }

    @Override
    protected void initView() {

    }

    @Override
    protected void initObservable(HvacViewModel viewModel) {
        viewModel.getTempLive().observe(this, new Observer<String>() {
            @Override
            public void onChanged(String temp) {
                LogUtils.logI(TAG, "[onChanged] " + temp);
            }
        });
    }

    @Override
    protected void loadData(HvacViewModel viewModel) {
        viewModel.requestTemperature();
    }
}


<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.mvvm.hmi.ipc.ui.HvacViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/btn_confirm"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="36dp"
            android:onClick="@{viewModel::setTemperature}"
            android:text="確定"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/et_temperature" />

        <EditText
            android:id="@+id/et_temperature"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="@={viewModel.tempLive}"
            app:layout_constraintBottom_toBottomOf="@+id/textView"
            app:layout_constraintStart_toEndOf="@+id/textView"
            app:layout_constraintTop_toTopOf="@+id/textView" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="24dp"
            android:layout_marginTop="24dp"
            android:text="Temperature:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

以上就是如何封裝一個適合車載應(yīng)用使用的 MVVM 框架。不知道你有沒有發(fā)現(xiàn)财忽,在HMI中使用AIDL方法倘核。通常是比較麻煩的。我們需要在HMI與Service完成綁定后即彪,我們才能調(diào)用Service中實(shí)現(xiàn)的Binder方法紧唱。但是示例中我們使用的SDK,并沒進(jìn)行綁定操作隶校,而是直接進(jìn)行調(diào)用漏益。關(guān)于如何編寫基于AIDL的SDK,就放到下一章再介紹深胳,感謝您的閱讀绰疤。

本文所涉及的源碼請訪問:https://github.com/linux-link/CarMvvmArch

參考資料

應(yīng)用架構(gòu)指南 | Android 開發(fā)者 | Android Developers

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市稠屠,隨后出現(xiàn)的幾起案子峦睡,更是在濱河造成了極大的恐慌,老刑警劉巖权埠,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榨了,死亡現(xiàn)場離奇詭異,居然都是意外死亡攘蔽,警方通過查閱死者的電腦和手機(jī)龙屉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來满俗,“玉大人转捕,你說我怎么就攤上這事∷衾” “怎么了五芝?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辕万。 經(jīng)常有香客問我枢步,道長,這世上最難降的妖魔是什么渐尿? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任醉途,我火速辦了婚禮,結(jié)果婚禮上砖茸,老公的妹妹穿的比我還像新娘隘擎。我一直安慰自己,他們只是感情好凉夯,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布货葬。 她就那樣靜靜地躺著,像睡著了一般劲够。 火紅的嫁衣襯著肌膚如雪宝惰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天再沧,我揣著相機(jī)與錄音尼夺,去河邊找鬼。 笑死炒瘸,一個胖子當(dāng)著我的面吹牛淤堵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顷扩,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼拐邪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了隘截?” 一聲冷哼從身側(cè)響起扎阶,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤汹胃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后东臀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體着饥,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年惰赋,在試婚紗的時候發(fā)現(xiàn)自己被綠了宰掉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡赁濒,死狀恐怖轨奄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拒炎,我是刑警寧澤挪拟,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站击你,受9級特大地震影響舞丛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜果漾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一球切、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绒障,春花似錦吨凑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至庐镐,卻和暖如春恩商,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背必逆。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工怠堪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人名眉。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓粟矿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親损拢。 傳聞我的和親對象是個殘疾皇子陌粹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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