Mike按:

前一段时间,在工作学习方面有点迷茫。请教了两个高人。分别给出了两个互补的建议,受益匪浅,感谢!一是学习开源项目,二是在应用的实际开发中学习东西(按照自己的思路写一个app)。最近主要是优化重构之前的代码,将开源项目中比较好的部分应用到自己的项目中。本文就是基于此。

 

(一) 应用场景:刷新数据,显示加载进度条,数据准备,数据准备完毕,进度条消失,显示结果。

可能出现的状况:

1, 无网络,无法获取数据,有网络,显示提示

2, 一切正常,显示数据

3, 搜索无结果,各种错误:协议错误,超时

之前的思路,先显示Dialog,新启线程加载数据,数据加载完毕后,Handler发送消息,UI中刷新,Dialog消失。结果的显示使用帧布局。功能实现上ok,但是封装上不好,感觉代码分散,混乱。

 

(二) Jamendo的处理

将这个处理过程封装成LoadingDialog(继承AsyncTask),结果的显示上使用ViewFlipper。思路非常清晰。

1, 加载处理的基类LoadingDialog

a) Jamendo将加载的处理均使用LoadingDialog的实现子类。相关继承树(extend tree)如下图所示:

b) LoadingDialog继承AsyncTask,这样的好处是容易控制流程的先后顺序,而且UI线程和其他线程的切换非常的平滑。其类的结构如下图所示:

 

c) 定义

泛型的使用,代码如下:

 
  
  1. public abstract class LoadingDialog<Input, Result> extends AsyncTask<Input, WSError, Result> 

d) 流程

 绘制其流程图如下:

 

 

i. onPreExecute()

 
  
  1. @Override 
  2.     public void onPreExecute() { 
  3.         String title = ""
  4.         String message = mActivity.getString(mLoadingMsg); 
  5.         mProgressDialog = ProgressDialog.show(mActivity, title, message, truetruenew OnCancelListener(){ 
  6.  
  7.             @Override 
  8.             public void onCancel(DialogInterface dialogInterface) { 
  9.                 LoadingDialog.this.cancel(true); 
  10.             } 
  11.  
  12.         }); 
  13.         super.onPreExecute(); 
  14.     } 

显示Dialog,这个创建方法第一次见到,Dialog可被cancel掉,代码如下:

 
  
  1. @Override 
  2.     public void onCancelled() {      
  3.          
  4.         if( mActivity instanceof PlayerActivity) 
  5.             { 
  6.             PlayerActivity pa = (PlayerActivity)mActivity; 
  7.             pa.doCloseActivity(); 
  8.             } 
  9.          
  10.         failMsg(); 
  11.         super.onCancelled(); 
  12.     } 

ii. doInBackground()-------抽象方法

这里是处理数据的地方,Jamendo在这里定义了抽象方法doInBackGround(),以应对不同的需求,子类各自实现即可。代码如下:

 
  
  1. @Override 
  2.     public abstract Result doInBackground(Input... params); 

iii. onPostExecute()

代码如下:

 
  
  1. @Override 
  2.     public void onPostExecute(Result result) { 
  3.         super.onPostExecute(result); 
  4.  
  5.         mProgressDialog.dismiss(); 
  6.  
  7.         if(result != null){ 
  8.             doStuffWithResult(result); 
  9.         } else { 
  10.              
  11.             if( mActivity instanceof PlayerActivity) 
  12.                 { 
  13.                 PlayerActivity pa = (PlayerActivity)mActivity; 
  14.                 pa.doCloseActivity(); 
  15.                 } 
  16.             failMsg(); 
  17.  
  18.         } 
  19.     } 

数据获取结束之后,Dialog dismiss掉。数据获取有两种情况:

l 获取数据正常

调用doStuffWithResult(Result result)方法处理数据,此方法为抽象方法,需要子类实现,按照自己的需求处理,代码如下:

 
  
  1. /** 
  2.      * Very abstract function hopefully very meaningful name, 
  3.      * executed when result is other than null 
  4.      *  
  5.      * @param result 
  6.      * @return 
  7.      */ 
  8.     public abstract void doStuffWithResult(Result result); 

l 获取数据异常

调用failMsg()方法。即Toast,代码如下:

 
  
  1. protected void failMsg(){ 
  2.         Toast.makeText(mActivity, mFailMsg, 2000).show(); 
  3.     } 

iv. onProgressUpdate()

显然,doInBackGround()方法中,可以实时的将一些信息(错误信息即WSError的Message)publish到本方法 。一旦出现问题:首先,toast,其次,取消本次异步任务,最后,Dialog dismiss掉。代码如下:

 
  
  1. @Override 
  2.     protected void onProgressUpdate(WSError... values) { 
  3.         Toast.makeText(mActivity, values[0].getMessage(), Toast.LENGTH_LONG).show(); 
  4.         this.cancel(true); 
  5.         mProgressDialog.dismiss(); 
  6.         super.onProgressUpdate(values); 
  7.     } 

2, 实现SearchDialog(以此为例,说明其实现,只说明最重要的)

a) SearchActvity内容部分的布局,效果图,参看上面彩图。

布局虽是自定义ViewFlipper,我在实际使用中使用原生ViewFlipper也ok,Xml文件如下:

 
  
  1. <com.teleca.jamendo.util.FixedViewFlipper 
  2.             android:id="@+id/SearchViewFlipper" 
  3.             android:layout_width="fill_parent" 
  4.             android:layout_height="fill_parent" 
  5.             android:layout_weight="1" 
  6.             android:background="#fff" > 
  7.  
  8.             <ListView 
  9.                 android:id="@+id/SearchListView" 
  10.                 android:layout_width="fill_parent" 
  11.                 android:layout_height="fill_parent" 
  12.                 android:divider="#000" /> 
  13.  
  14.             <TextView 
  15.                 android:layout_width="wrap_content" 
  16.                 android:layout_height="wrap_content" 
  17.                 android:layout_gravity="center" 
  18.                 android:text="@string/no_results" > 
  19.             </TextView> 
  20.  
  21.             <TextView 
  22.                 android:layout_width="wrap_content" 
  23.                 android:layout_height="wrap_content" 
  24.                 android:layout_gravity="center" 
  25.                 android:text="@string/search_list_hint" > 
  26.             </TextView> 
  27.         </com.teleca.jamendo.util.FixedViewFlipper> 

b) 多态的使用

我是在这个类中深深体会多态的妙处。搜索有很多的分类,每一类可能对应不同的Adapter,那么对结果的处理就成了一个问题。在SearchDialog中定义BaseAdapter。对于所有的搜索结果的处理就smooth了。代码如下:

 
  
  1. AlbumAdapter albumAdapter = new AlbumAdapter(SearchActivity.this);  
  2.                 albumAdapter.setList(albums); 
  3.                 albumAdapter.setListView(mSearchListView); 
  4.                 mAdapter = albumAdapter; 

c) 错误信息的捕获

Jamendo自己定义了Throwable类WSError,捕获Exception之后(就是主要的网络连接错误:请求异常,连接超时,下载超时等),将错误信息封装之后,向上抛出。分两步处理方式(以AlbumSearch为例):

第一步:catch

第二步:publishProgress

代码如下:

 
  
  1. catch (JSONException e) { 
  2.                 e.printStackTrace(); 
  3.             } catch (WSError e) { 
  4.                 publishProgress(e); 
  5.                 this.cancel(true); 
  6.             } 

d) 对结果的处理

多态的使用,使得ListView的数据填充很easy。代码很清晰,不用作解释,代码如下:

 
  
  1. @Override 
  2.         public void doStuffWithResult(Integer result) { 
  3.             mSearchListView.setAdapter(mAdapter); 
  4.  
  5.             if(mSearchListView.getCount() > 0){ 
  6.                 mViewFlipper.setDisplayedChild(0); // display results 
  7.             } else { 
  8.                 mViewFlipper.setDisplayedChild(1); // display no results message 
  9.             } 
  10.  
  11.             // results are albums 
  12.             if(mSearchMode.equals(0) || mSearchMode.equals(1) ||  mSearchMode.equals(3)){ 
  13.                 mSearchListView.setOnItemClickListener(mAlbumClickListener); 
  14.             } 
  15.  
  16.             // results are playlists 
  17.             if(mSearchMode.equals(2)){ 
  18.                 mSearchListView.setOnItemClickListener(mPlaylistClickListener); 
  19.             } 
  20.         } 

3, 搜索状态的保存

Jamendo对搜索状态的保存,是基于其的搜索页面是Activity之间的跳转。所以有必要保存。不做解释了,代码如下:

 
  
  1. @SuppressWarnings("unchecked"
  2.     @Override 
  3.     protected void onRestoreInstanceState(Bundle savedInstanceState) { 
  4.         mSearchMode = (SearchMode) savedInstanceState.getSerializable("mode"); 
  5.         if(mSearchMode != null){ 
  6.             if(mSearchMode.equals(SearchMode.Artist)  
  7.                     || mSearchMode.equals(SearchMode.Tag) 
  8.                     || mSearchMode.equals(SearchMode.UserStarredAlbums)){ 
  9.                 AlbumAdapter adapter = new AlbumAdapter(this); 
  10.                 adapter.setList((ArrayList<Album>) savedInstanceState.get("values")); 
  11.                 mSearchListView.setAdapter(adapter); 
  12.                 mSearchListView.setOnItemClickListener(mAlbumClickListener); 
  13.             } 
  14.              
  15.             if(mSearchMode.equals(SearchMode.UserPlaylist)) { 
  16.                 PlaylistRemoteAdapter adapter = new PlaylistRemoteAdapter(this);  
  17.                 adapter.setList((ArrayList<PlaylistRemote>) savedInstanceState.get("values")); 
  18.                 mSearchListView.setAdapter(adapter); 
  19.                 mSearchListView.setOnItemClickListener(mPlaylistClickListener); 
  20.             } 
  21.              
  22.             mViewFlipper.setDisplayedChild(savedInstanceState.getInt("flipper_page")); 
  23.         } 
  24.         super.onRestoreInstanceState(savedInstanceState); 
  25.     } 
  26.  
  27.     @Override 
  28.     protected void onSaveInstanceState(Bundle outState) {//保存上次的搜索结果,返回时还在 
  29.         if(mSearchMode != null){ 
  30.             outState.putSerializable("mode", mSearchMode); 
  31.             if(mSearchMode.equals(SearchMode.Artist)  
  32.                     || mSearchMode.equals(SearchMode.Tag)  
  33.                     || mSearchMode.equals(SearchMode.UserStarredAlbums)){ 
  34.                 AlbumAdapter adapter = (AlbumAdapter)mSearchListView.getAdapter(); 
  35.                 outState.putSerializable("values", adapter.getList()); 
  36.             } 
  37.  
  38.             if(mSearchMode.equals(SearchMode.UserPlaylist)) { 
  39.                 PlaylistRemoteAdapter adapter = (PlaylistRemoteAdapter)mSearchListView.getAdapter(); 
  40.                 outState.putSerializable("values", adapter.getList()); 
  41.             } 
  42.              
  43.             outState.putInt("flipper_page", mViewFlipper.getDisplayedChild()); 
  44.         } 
  45.         super.onSaveInstanceState(outState); 
  46.     } 

(三) 思考

前面的博文分析提到,Jamendo中有一个RequestCache,缓存10次请求。觉得意义不大,因为点开任意一个页面,滚动一下,很快就超过10个url请求。在Search这里,发现RequestCache的好处。搜索在短时间内,结果基本不会发生差别,所以阻止重复搜索很有必要。在实时性要求特别高的weibo里,RequestCache的使用也不用影响其刷新到最新数据,因为10次很容易超过,之前保留的刷新最新数据的老数据将被remove掉。

 

注:重构代码时,发现一个奇怪的问题,当前Activity有EditText(即有组件被focus到时),其继承的BaseActivity的无法显示showDialog()所对应的Dialog不会显示,但new出的Dialog可以显示。查看文档后,showDialog()方法不建议使用。文档如下: