Android:“萬能”Activity重構篇

android

前言


Android:你是如何把Activity寫的如此“萬能”的這篇文章到現在已經好久了,但是由于最近事情較多,寫重構篇的計劃就一直被無情的耽擱下來了,借這幾天還算有點空余時間,把自己這樁心事了解下。

其實大家也知道Android:你是如何把Activity寫的如此“萬能”的這篇文章只是個引子,其實我真正想引出的是mvp設計模式,因為最近自己最近在用mvp做項目,自己對mvp有一些感悟,因此我將用mvp進行“萬能”activity的重構。

同時也有一些朋友與我交流mvp,他們會被mvp中的m,v,p都應該放什么邏輯而困惑?會被mvp中的m應該怎么寫而困惑?會被一個界面,怎么按照mvp來進行重構感到困惑?會被listview的adapter應該放在m層還是p層困惑?

我希望通過本文的講解能幫助大家對mvp有一個更深的了解,現在進入主題內容。

正文內容


本文內容分為2部分:

  • 帶你了解mvp
  • 使用mvp對“萬能”Activity進行重構

第一部分會深入了解mvp到底是什么,它的好處等知識。第二部分會講解如何利用mvp來重構“萬能”Activity。

帶你了解mvp


任何軟件都是以數據為中心,為了能與用戶進行交互,就需要提供界面支持用戶對數據進行增刪改查操作。
不管是mvc,mvp還是mvvm始終都在做一件事情:怎么樣能更好的解決數據與界面之間的關系,以達到數據與界面之間的耦合更低,代碼的復用性更高,代碼的可測性更好。
本文的重點是講解mvp,因此讓我們開始了解下mvp是怎么組織數據與界面之間的關系的。我們先從mvp的結構圖說起。

mvp的面容


網上有些關于mvp的結構圖基本是以下樣子


mvp結構圖.png

我覺得這張圖是有問題的,問題在于presenter把請求轉交給model,model應該把處理結果返回給presenter,這張圖是沒有反映這個過程的。

正確的mvp的結構圖是這樣子的

mvp結構

我們先看下能從這張圖中得到哪些信息?

  • mvp的分層結構特別類似于網絡的七層協議,每層只知道自己依賴層的細節。
  • 這種分層的好處是:層與層之間的耦合性低,模塊的復用性高,可維護性更好,每層可以單獨存在,這樣可測性更好。
  • 數據流的走向可以是:view-->presenter-->model-->presenter-->view,這種數據流一般出現的場景是用戶在界面觸發了一個事件的情形下
  • 數據流的走向也可以是:model-->presenter-->view,這種數據流一般出現于比如通過長鏈接接收消息的場景。
  • 不管數據流是怎樣的一個流動走向,始終有一個原則是:數據流不能跨層流動,即層與層不能跨層通信。

看了mvp的整體結構圖,我們以從底層到上層的順序依次來介紹model,presenter,view。

model


先說下一些關于model的錯誤理解:

  • model是實體類的集合
  • 比如從json中解析數據的代碼應該放在presenter中
  • model與mvc中的model是一樣的

關于model的正確理解我們會在文中看到。

數據加工處理廠
通過應用mvp后的感受,我個人的感覺model是最難寫的一層,并且也是最難懂的,因為model是整個應用或界面的數據加工處理廠,所謂數據加工廠就是對數據的獲取,數據的解析,數據的存儲,數據的分發,數據的增刪改查等操作。意思就是凡是涉及到數據操作都是在model進行的,所以model不僅僅只是實體類的集合,同時還包含關于數據的各種處理操作。

三種數據源
數據的數據源有三種:內存,磁盤(文件或數據庫等),網絡。為了提升app的性能,有必要把經常訪問的數據臨時存入內存中;同時也為了提升app性能和為用戶省流量省電,有必要把數據存入磁盤中;還有的數據是有必要從網絡讀取的。三個數據源不一定同時存在,比如不與網絡交互的app,不存在網絡數據源。所以凡是涉及到關于數據發生于三個數據源加工處理的操作的代碼都要放在model中。

model為上層提供的服務
model從黑盒的角度來看為上層(指依賴于model的層比如present)提供的服務無非就2種:model為上層提供數據,model處理上層傳遞的數據

model為上層提供數據
上層會從model中去數據,那model會從三數據源中取數據,取的順序是

  • 先內存,內存取到數據返回
  • 其次磁盤,磁盤取到數據,如有必要把數據存儲在內存中,則需要進行
    存儲,返回數據
  • 最后網絡,網絡取到數據,如有必要在磁盤或內存中存儲,則進行存儲,返回數據

上面的取數據過程是最簡單的情況,復雜些還會涉及到從內存或磁盤中取到的數據是否過期,過期的話就應該從網絡獲取。從網絡取得數據后需要把內存或磁盤的數據更新。

model處理上層傳遞的數據
model接收到上層傳遞的數據后,model會依次把數據扔給三個數據源去處理,有可能三個數據源都會處理數據,有可能只是其中一個處理,model會把處理的結果返回。

所以model會把解析好的數據提供給上層,上層對于數據的來源完全是透明的,上層完全不需要關心數據到底是來自內存,還是磁盤甚至是網絡。同理上層只需要的把數據扔給model,上層唯一做的事情就是愉快的等待處理結果。

tip
mvc中的model是要和view進行交互的,而mvp中的model不會知道任何view的細節。
model中的所有操作都發生于普通線程。

關于model的介紹先到此,我們在來看下presenter。

presenter


presenter翻譯成漢語的意思是主持人,提出者。從它的意思可以看出它有控制全場的作用。首先presenter是處于mvp的中間層,在view和model中起一個承上啟下的作用。presenter會把view交給自己的命令進行一定的校驗等操作交給model處理,會把model處理的結果交給view。

presenter封裝業務
presenter不僅起一個橋梁的作用,它還會把業務邏輯代碼給包攬下來。這樣就可以減輕Activity的負擔了,讓Activity全心全意做它的view工作。那估計就有朋友犯迷糊了,哪些代碼屬于業務邏輯呢?比如一些校驗代碼?;蛘呖梢赃@樣想只要是不屬于view和model的代碼基本都可以放在presenter中。

presenter負責刷新view
mvc或以前的關于view的寫法一般都是這樣,view在接收到數據后,自己來進行view的刷新或其他操作。但是mvp中presenter負責對view進行刷新,比如從model獲取的數據,presenter會根據獲取的數據成功與否來通知view應該是顯示成功界面還是失敗界面。這樣就讓Activity變的更輕了,變成了聽別人指揮的傻白甜了。這時候的presenter就有點主持人,掌控者的味道了。

presenter持有的線程
Android中view的操作需要在ui線程里執行,其他耗時操作需要在普通線程執行。presenter會持有這2種線程:ui線程,普通線程。刷新view時,它切換為ui線程進行刷新,從model取數據切換為普通線程。假如使用rxjava的話,就特別簡單了關于線程切換的事情。

tip
presenter從model中獲取的數據就是解析好的數據,不需要出現解析數據的代碼。

接著我們來看下view。

view


view層就很好理解了,就是用戶直接看到的界面,mvp中的view是很省心的,比如更新view,接收數據。這些操作它都不需要操心,也不需要知道數據到底來自哪里,給我啥我顯示啥就可以了。
一個view可以同時擁有多個presenter,也可以只有一個presenter。
Android中的Activity,Fragment在mvp中是作為view來使用的,這些Activity,Fragment的責任就小了,只關心界面相關的事情足矣。

各種Adapter是放在view層的。

總結


我們初步認識了mvp,mvp中的model,present,view到底是什么,他們之間的關系是什么樣的,這只是初步認識mvp,關于mvp中還有很多細節需要介紹,比如android clean architecture 中model和presenter之間多了一層interactor,多的這層interactor是用來做什么的,model層是怎么架構的。google mvpmodel層要比android clean architecture 簡單等,希望能在我后面的章節看到相關關于每層的詳細介紹。我們開始進入我們的重構"萬能"Activity的部分。

使用mvp設計模式對"萬能"Activity進行重構


回憶下“萬能”Activity的樣子

我在上篇文章的“萬能”的LoginActivity基礎上增加了登錄對話框的功能,“萬能”LoginActivity的代碼如下:

  public LoginActivity extends Activity{
          private EditText  mUserNameView, mPasswordView;
          private Button mLoginView;

          public void initViews(){
                 .......
                  各種findViewById.....代碼
                  
                  //給登陸按鈕加監聽器
                  mLoginView.OnClickListener(new View.OnClickListener() { 
                        @Override 
                         public void onClick(View v) {  

                              String userName = mUserNameView.getText();
                              String password = mPasswordView.getText();
                              //驗證用戶輸入的密碼是否合法
                              if(!validate(userName) || !validate(password)){
                                  告訴用戶輸入的用戶名或密碼不合法
                              }  else{
                                  //開始登陸
                                  login(userName,password);
                              }
                        }
                  });
          }

           //登陸方法,用偽代碼來寫下網絡請求
           private void login(String userName,String password){
          
                //增加登錄進度對話框給用戶友好用戶體驗
                顯示登錄進度對話框...

                HttpClient.getInstance().login(userName,password,
                   new ResponseListener(){
                          
                           public void failed(Failed failed){

                                  把登錄進度對話框消失...

                                  做失敗相關的處理工作,比如給用戶提示
                                  把密碼輸入框清空,還比如登陸次數限制等
                           }

                           public void success(Response response){

                                   把登錄進度對話框消失...

                                   做成功相關的處理工作
                                  //暫且把用戶信息的類叫做UserInfo,從json中解析數據,假設response.getContent()存在
                                  String jsonContent = response.getContent();
                                  JsonObject jsonObject = new JsonObject(jsonContent);
                                  UserInfo userInfo = new UserInfo();
                                  userInfo.name = jsonObject.optString("name");
                                  userInfo.userId = jsonObject.optString("userId");
                                  其他字段的解析......
                                  //保存userInfo信息到數據表中,假設userDatabase已經存在
                                  userDatabase.save(userInfo);

                                  跳到app的主頁
                           }
                 });
           }

            //驗證給定的字符串是否合法,true 合法,false 不合法
           private  boolean validate(String str){

           }
   }

我們回憶了“萬能”LoginActivity的代碼后,開始重構。

開始重構

model
在使用mvp時,我一般有個習慣就是首先從model->presenter->view的順序寫代碼,所以重構“萬能”LoginActivity也先從model開始。前半部分關于model介紹過,model從黑盒的角度來說只有2個功能:一個是輸出數據,一個是輸入數據。因此登錄中presenter只需要把賬號,密碼交給model,presenter唯一做的事情就是監聽登錄狀態即可。model會把presenter傳遞的賬號,密碼交給服務器,model在把服務器返回的數據進行解析,存儲在磁盤或內存中,把解析好的數據傳遞給presenter。那我們看下偽代碼:

   //管理登錄的類,它是單例的,這就不寫單例方法了
   public class LoginManager{
          
        
          //登錄的監聽器
          public static interface LoginListener{
                  //登錄成功
                  void onSuccessLogin(UserEntity user);
                  //登錄失敗
                  void onFailedLogin(Failed failed);
          }

          //登錄方法
          public void login(String name,String password,final LoginListener loginListener){

                   //假設HttpClient是一個請求網絡服務的類
                   HttpClient.getInstance().login(userName,password,
                   new ResponseListener(){
                          
                           public void failed(Failed failed){

                                  loginListener.onFailedLogin(failed);
                           }

                           public void success(Response response){
                                  //假設UserParse類已經存在,主要用來從response中解析UserEntity
                                  UserEntity userEntity = UserParse(response.getContent());
                                  //假設userDatabase是數據庫存儲類
                                  userDatabase.store(userEntity);
                                  //還可以把userEntity存入內存中,這得根據業務需求進行處理
                                  loginListener.onSuccessLogin(userEntity);
                           }
                 });

                    
          }
   }

登錄的model層我們沒有做的那么復雜,比如把服務器返回的用戶信息存儲在內存中,把服務器返回的token存儲在磁盤中,實現自動登錄功能等,本例子只是一個特別簡單的登錄功能,實際應用中登錄需要考慮很多的東西,登錄的modle層到此重構完畢。

presenter
上文中提到過presenter,presenter起連接view與model的作用,presenter封裝業務作用,presenter有負責刷新view的作用。
我們梳理下presenter都應該包含哪些功能:

  • 驗證賬號,密碼的合法性.
  • 把驗證成功的賬號,密碼交給model層
  • 把登錄的狀態傳遞給view層,并根據不同的登錄狀態通知view顯示不同的界面

那讓我們開始寫代碼,presenter層的類組織結構我是參照google mvp的presenter類組織結構來進行的,因為我認為google mvp presenter類結構更清晰,看下偽代碼:

   //登錄的條約接口,分別定義了登錄view的一些方法,和登錄presenter的一些方法     
   public interface LoginContract{
            //需要view層來實現的登錄view接口,IView是所有view的基類
            interface ILoginView extends IView{

                  void onShowSuccessLoginView(UserInfo userInfo);
                  void onShowFailedLoginView(int failed);
                  void showLoginingView();
                  void dissLoginingView();
            }
            
            //定義了登錄presenter的一些方法,IPresenter是所有Presenter的基類
            interface ILoginPresenter extends IPresenter<ILoginView>{
                  void login(String name,String password);
            }
    }

    public  interface IView{
            
            void initView();
    }

    //IPresenter提供了一些基礎方法,其實這些方法是對應Activity或Fragment的生命周期方法
    public interface IPresenter<V  extends IVew>{
            
              void onStop(); 
              void onResume(); 
              void onDestroy();
              void onPause();
              void onStart();
              void init(V view);
    }

    //登錄的presenter
    public class LoginPresenter implements ILoginPresenter{

            private ILoginView mLoginView;
            private LoginManager mLoginManager = LoginManager.getInstance();
            public void init(ILoginView loginView){
                    mLoginView = loginView;
                    mLoginView.initView();
            }

            public void login(String name,String password){
                    //驗證name,password的合法性,
                    if(validate(name) && validate(password)){
                      //假設NormalThread.exe方法可以讓操作在普通線程里執行        
                      mLoginView.showLoginingView();
                      NormalThread.exe(new   Runnable(){
                          public void run(){
                              mLoginManager.login(name,password,
                                new LoginListener(){
                                        
                                        public void onSuccessLogin(UserEntity userEntity){
                                                //UserMapper類,負責把底層的UserEntity轉化為view層使用的UserInfo
                                                UserInfo userInfo = UserMapper.map(userEntity);
                                                //下面的代碼在ui線程中執行,這就不寫具體的實現了
                                                mLoginView.onShowSuccessLoginView(userInfo);
                                                mLoginView.dissLoginingView();
                                                                                   
                                        }

                                        public void onFailedLogin(Failed failed){
                                             //下面的代碼在ui線程中執行,這就不寫具體的實現了
                                                mLoginView.onShowFailedLoginView(failed.failedState);
                                             mLoginView.dissLoginingView();
                                        }
                                });
                            }
                         }
                     }else{
                            //假設1代表賬號,密碼不合法
                            mLoginView.onShowFailedLoginView(1);
                     }
            }
    }

以上登錄的Presenter層的偽代碼都是關鍵代碼,讓我們看下以上代碼都做了什么?

  • LoginContract 把ILoginView和ILoginPresenter組合在一塊,ILoginView定義了提供給ILoginPresenter的方法,ILoginPresenter定義了提供給ILoginView的方法。只需要看LoginContract就可以知道登錄的view層和presenter層之間的約定。我很喜歡XXContract類,也推薦大家使用
  • IView是一個基礎接口,提供一些公用方法
  • IPresenter是一個基礎接口,它把Activity或Fragment的生命周期方法集合了起來。有了這些生命周期方法,presenter就讓Activity一心一意做它的view相關的工作。

到此登錄Present的重構結束,我們重構view層。

view
view層就很簡單了,只是需要把基礎設施建立好,直接看偽代碼:

    public abstract class BaseActivity extends FragmentActivity{

          private Set<IPresenter> mAllPresenters = new HashSet<IPresenter>(1);

          /** * 獲取layout的id,具體由子類實現
           * @return
           */
          protected abstract int getLayoutResId();
          
          /**
          *需要子類來實現,獲取子類的IPresenter,一個activity有可能有多個IPresenter
           */
          protected abstract IPresenter[] getPresenters();

          //初始化presenters,
          protected abstract void onInitPresenters();

          /** * 從intent中解析數據,具體子類來實現
         * @param argIntent
         */
          protected void parseArgumentsFromIntent(Intent argIntent){
          }     

          private void addPresenters(){

                  IPresenter[] presenters = getPresenters();
                  if(presenters != null){
                          for(int i = 0; i < presenters.length; i++){
                                mAllPresenters.add(presneters[i]);
                          }
                  }
          }         

          @Override    
          protected void onCreate(@Nullable Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);            
              setContentView(getLayoutResId());
              if(getIntent() != null){
                  parseArgumentsFromIntent(getIntent());
              }  
              addPresenters();
              
             onInitPresents();
          }

          @Override
          protected void onResume() { 
               super.onResume(); 
                //依次調用IPresenter的onResume方法
               for (IPresenter presenter:mAllPresenters  ) { 
                     if(presenter != null){   
                          presenter.onResume(); 
                     }
              }
           }

            ...其他生命周期方法也是類似,調用IPresenter中相應的生命周期方法...

    }

基礎設施已經ok了,這時候我們就該重構"萬能“LoginActivity了。

    public class LoginActivity extends BaseActivity implements LoginConstract.ILoginView{

            private LoginPresenter mLoginPresenter = new LoginPresenter();
            protected int getLayoutResId(){
                  return R.layout.activity_login;
             }

            protected IPresenter[] getPresenters(){
                  return new IPresneter[]{ mLoginPresenter};
            }

           //初始化presenters,
           protected void onInitPresenters(){
                  mLoginPresenter.init(this);
           
           }

           public void initView(){
                ...初始化view的代碼...
                //
                mLoginButton.setOnClickListener(new OnClickListener() {
                      @Override
                      public void onClick(View v) { 
                          
                              mLoginPresenter.login(name,password);
                      }
                });
           }

           public void onShowSuccessLoginView(UserInfo userInfo){
                    ....顯示登錄成功界面....
           }
                  
            public  void onShowFailedLoginView(int failed){
                    ...顯示登錄失敗界面...
            }
            
            public  void showLoginingView(){
                    ...顯示登錄進度條對話框...
            }
            
            public   void dissLoginingView(){
                    ...消失登錄進度條對話框...
            }

    }

我們著重說下基礎設施BaseActivity:
因為一個Activity是有可能包含多個Presenter的,所以需要在BaseActivity中是有必要把這些Presenter收集起來。
需要在BaseActivity的生命周期方法里面調用每個Presenter的相應周期方法。

重構以后的LoginActivity是不是很清爽,只保留與view相關的邏輯。

小結重構后的類結構
重構“萬能”LoginActivity到此結束,我們小結下重構后的類結構:

  • model層包含:LoginManager(登錄管理類),UserEntity(用戶實體類,主要使用在model層),UserDatabase(用戶數據表)。
  • presenter層包含:LoginConstract(登錄約定類,組合ILoginView和ILoginPresenter),LoginPresenter(登錄presenter類),UserMapper(負責把model層的UserEntity轉化為view層使用的
    UserInfo)
  • view層包含:BaseActivity(基礎類),LoginActivity(登錄Activity),UserInfo(用戶信息類,主要使用在view層)

重構感悟

疑惑
估計會有細心的朋友發現model層是LoginManager而不是像android clean architecture 中model層使用了respository,我個人覺得model層也沒必要這么嚴格的按respository的架構方式來組織類結構,因為本例中登錄功能實在是太簡單了,所以就用最簡單的一個LoginManager類來供上層調用。

優缺點
使用mvp重構“萬能”Activity以后,帶來了以下好處:

  • 類的組織結構更清晰
  • 每層可以進行單獨的測試
  • 類的可復用性更高
  • 層與層的耦合性降低
  • 可維護性更高
  • 每個類盡量做到單一職責

但同時也帶來了一些缺點,比如創建的類多了很多,管理這些類的成本會增加。但是萬事萬物都有兩面性,就看利與弊的大小了。我個人覺得mvp的利肯定是大于弊的,所以有必要采用這種架構來設計你的app。

提高生產效率

以上偽代碼中我沒有使用rxjava,dagger2,假如把它們應用于mvp中會讓你事半功倍,rxjava可以讓你在寫presenter層和model層時,可以讓presenter與model交互更簡單,可以是model層變的尤為的簡單比如從三大數據源取數據操作,我們自己用代碼實現是可以的但是畢竟要花很多時間,但是用rxjava的一些操作符很容易做到。dagger2可以更好的幫助你進行依賴注入, 可以參考下Android:dagger2讓你愛不釋手-基礎依賴注入框架篇
的介紹,還有鼎鼎有名的retrofit也是可以提高效率的。

總結


本文我們了解了mvp以及每一層,以及使用mvp來重構“萬能”Activity,其實每一層需要注意的東西還有很多,比如model層是最難寫的一層。希望我能在以后用幾篇文章來介紹每一層更多的細節。還有我現在正在用mvp做的一個項目,等做完了我會上傳到github。歡迎大家指正。

本人微信:704451290

本人公眾賬號
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,660評論 25 708
  • 轉載至:http://www.lxweimin.com/p/9a6845b26856 “Android MVP 詳解...
    SnowDragonYY閱讀 10,329評論 5 241
  • 作者:李旺成 時間:2016年4月3日 “Android MVP 詳解(下)”已經發布,歡迎大家提建議。 MVP ...
    diygreen閱讀 128,931評論 86 1,321
  • 寫下這個標題,我的心里難免有點心虛,我覺得自己都算不上一個有素養的人,但是心里還是有一種想寫寫的鬼祟的心里,就算是...
    禽流感閱讀 698評論 1 1
  • 2017年6月25日 英:“我下周離職,想去甘南玩,你去嗎?” 我:“甘南倒是蠻好奇的,可是7月3號我和別人約好了...
    林瀟Ena閱讀 378評論 1 3