文章目錄
1.引言
最近一段時間由於畢設以及答辯等一系列的事情,已經很久沒有更新博客了。在月初立下的Flag——每天學習一個Android中的常用框架,也沒能堅持下去。當然,作者並不是一個半途而廢的人。等到最近的事情完成得差不多了,還是會繼續更新這個系列的博文。
事實上,在處理事情的同時,我研究了App的幾種開發模式,並且嘗試學習並運用其中的一些主流的技術棧。目前來說,App的開發模式主要分爲Native App(原生App)、Web App(WebApp)、HyBird App(混合App)。這三種App的開發模式在網上都有具體的介紹,感興趣的讀者可以查找一下相關的資料。
通過一段時間內對這三種開發模式的學習,爲了加深自己的印象,本着實踐出真知的想法,作者產生了使用這三種開發模式分別開發一個App的想法。一來是可以鞏固自己的基礎,二來也是提升自己的實踐運用能力。
經過一週的時間,作者成功根據三種App的開發模式開發出了文章名所說的資訊類App——《聽風資訊》。由於作者本人的不熟練,項目裏還存在相當多的可優化處。也因爲項目的不成熟,該項目的源碼就不公開放在碼雲上了,對項目感興趣的讀者可以私下聯繫我,QQ:545646733。
無圖無真相,接下來把分別通過這三種App開發模式的App運行效果及其目錄結構展示出來:
- Native App(原生App)
-
Web App(WebApp)
-
HyBird App(混合App)
該App的功能比較簡單,基本上就是仿照世面上常見的資訊類App。通過成果展示也可以看出,它們之間的共同功能有: -
網絡訪問接口數據(Json格式);
-
解析Json格式的數據,並且渲染到列表上;
-
對圖片加載的優化;
-
下拉刷新;
-
底部的導航欄;
-
點擊某條新聞時,進入該新聞對應的網頁;
-
循環展示的輪播圖;
-
頁面中的導航欄;
-
均實現了異步調用(即數據的獲取和UI的渲染是分開的)
-
沉浸式狀態欄
接下來會介紹項目裏這三種App開發模式的異同,以及作者開發過程中的一些感想。
2.App開發模式的主要區別
根據網絡上查詢的資料,三種App開發模式的主要區別如下:
性質/App類型 | Native App(原生App) | Web App(WebApp) | HyBird App(混合App) |
---|---|---|---|
技術棧 | Android | UniApp、Ionic、Cordova | React Native、Flutter |
語言 | Java、Kotlin | Html、Css、Js | ReactJs、Dart |
對應平臺 | Android | Android、IOS、微信小程序等多個平臺 | Android、IOS |
兼容性 | 高 | 差,不支持本地數據庫讀寫和驅動調用 | 一般 |
性能 | 高 | 差,大部分內容需要聯網纔可使用 | 一般 |
開發成本 | 高 | 低,大部分邏輯僅需要實現前端頁面即可 | 一般 |
3.App開發模式在開發項目時所使用到的技術棧
在資訊類App《聽風資訊》中,針對三種App開發模式的特點,分別選用瞭如下所示的技術棧來進行設計:
性質/App類型 | Native App(原生App) | Web App(WebApp) | HyBird App(混合App) |
---|---|---|---|
技術棧 | Android | UniApp | Flutter |
語言 | Java | Vue.js | Dart |
網絡訪問 | OkHttp | Promise | Dio |
圖片加載 | Glide | Image | Image |
數據解析 | Gson | / | FlutterJsonBeanFactory |
側拉欄 | DrawerLayout、NavigationView | Drawer | Drawer |
列表 | RecycleView、ViewPager | V-for | ListView、ListTitle |
狀態欄 | Toolbar | TitleNView | AppBar |
輪播圖 | Banner | Swiper | Swiper |
底部標籤 | BottomNavigationView | TabBar | BottomNavigationBar |
導航欄 | TabHost | / | TabBarView |
下拉刷新 | SwiperRefreshLayout | PullDownRefresh | RefreshIndicator |
異步模型 | Handler、AsyncTask | Await、Async | Future、Isolate |
沒有列出具體選項(即“/”)的格子即表示實現該功能還沒有較好的技術手段或者本身就支持了,其餘基本上都是當下較爲流行的框架,版本號也是各代碼倉庫(GitHub、DCloud、Pub等)裏最新(2020.6.22)的。
接下來,將會介紹作者在進行App開發時遇到的幾個難點。
4.App開發時的感想
4.1 Native App(原生App)
原生App,即使用Java或者Kotlin語言進行實現的Android應用。最早入坑Android時,接觸的基本上都是原生App。由於技術棧的原因,可以讓熟悉Java/Kotlin語言的人很快就學會Android的很多特性,從而開始Android應用的研發。
當然,作爲高度可定製並且兼容性最佳的開發模式,原生App基本上作爲當下Android應用的主流。但與此同時,也產生了諸如屏幕適配,大圖加載,資源裝載等許多細節問題。幸而目前原生App已經發展很成熟了,有許多好用的工具可以解決這些問題。
作爲Android工程師,最需要熟悉的App開發模式就是原生App了。記錄完這篇博客後,作者將會研究一個更爲成熟、好用的原生App腳手架,並通過另一篇博客進行記錄(立下flag)。
接下來,談談作者在進行原生App開發時遇到的一些主要難點:
4.1.1 Material Design的設計
4.1.1.1 BottomNavigationView
BottomNavigationView是Google官方提供的一種實現底部標籤切換的控件,在Android Studio 3.0之後就可以通過創建Activity中的Bottom Navigation Activity
,如圖所示:
最開始做項目時,要實現底部標籤需要使用RadioGroup + RadioButton來實現這部分的功能,RadioButton還需要編寫一個Selector來滿足圖標在點擊時顯示不同的圖樣,而使用BottomNavigationView似乎就能很好地解決這塊的問題。
當然,使用BottomNavigationView時,需要注意幾個要點:
-
在初始化BottomNavigationView管理着的Fragment時,如果你的項目中使用到了Toolbar,則需要先綁定Toolbar,否則會報空指針異常,代碼如下:
// 初始化ToolBar,注意要在Fragment初始化之前調用,不然會報空指針異常,這裏踩過坑! setSupportActionBar(tb_title); // 初始化Fragment initFragment();
-
id的對應。
<menu>
標籤和<item>
標籤中控件的id要對應,不然會不顯示內容。 -
另外,若使用BottomNavigationView,佈局則推薦使用ConstraintLayout,即約束佈局,這樣會比較好控制控件的擺放(這個控件作者本人用的也不是很熟練,在使用時遇到了BottomNavigationView遮擋RecyclerView的情況,導致內容顯示不全,最後作者用了很笨的方法才調整成功,希望有比較瞭解這塊內容的讀者能夠在評論區不吝賜教,作者將感激不盡)
4.1.1.2 Toolbar
Toolbar是Google官方提供的一種實現狀態欄切換的控件,作爲替換Actionbar的狀態欄,功能要更爲強大。
使用Toolbar時,需要注意幾個要點:
-
Android應用默認使用的是Actionbar,要使用Toolbar,記得在values/style.xml中聲明Android應用的樣式,即
NoActionBar
代碼如下:<!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorRed</item> <item name="colorPrimaryDark">@color/colorRed</item> <item name="colorAccent">@color/colorAccent</item> </style>
-
要通過Toolbar實現沉浸式狀態欄,只需要修改values/style.xml中的配置顏色,並且讓Toolbar的顏色也對應即可,代碼如下:
<item name="colorPrimary">@color/colorRed</item> <item name="colorPrimaryDark">@color/colorRed</item>
<androidx.appcompat.widget.Toolbar android:id="@+id/tb_title" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> </androidx.appcompat.widget.Toolbar>
-
Toolbar的標題默認是顯示在左邊的,要想顯示在正中,常見的做法是在Toolbar中嵌套一個居中顯示的TextView,這裏也可以使用一個工具類來調整Toolbar中標題的擺放,代碼如下:
public class ToolBarUtils { public static void setTitleCenter(Toolbar toolbar) { String title = "title"; final CharSequence originalTitle = toolbar.getTitle(); toolbar.setTitle(title); for (int i = 0; i < toolbar.getChildCount(); i++) { View view = toolbar.getChildAt(i); if (view instanceof TextView) { TextView textView = (TextView) view; if (title.equals(textView.getText())) { textView.setGravity(Gravity.CENTER); Toolbar.LayoutParams params = new Toolbar.LayoutParams(Toolbar.LayoutParams.WRAP_CONTENT, Toolbar.LayoutParams.MATCH_PARENT); params.gravity = Gravity.CENTER; textView.setLayoutParams(params); } } toolbar.setTitle(originalTitle); } } }
-
Toolbar默認是沒有返回按鈕的,若想開啓需要先調用
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
,然後實現其點擊方法,代碼如下:/** * 點擊Toolbar上的“回退”按鈕時觸發的邏輯 * @param item * @return */ @Override public boolean onOptionsItemSelected(MenuItem item) { if(item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); }
-
可以在設定Toolbar時用
?attr/actionBarSize
來界定其高度,代表之前應用還擁有Actionbr控件時的高度,控件整體代碼如下:<androidx.appcompat.widget.Toolbar android:id="@+id/tb_title" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> </androidx.appcompat.widget.Toolbar>
4.1.1.3 SwipeRefreshLayout
SwipeRefreshLayout是Google官方提供的一種實現下拉刷新的控件,作爲替換pullToRefresh的下拉刷新控件,使用和集成要相對簡單一些。
使用SwipeRefreshLayout時,需要注意幾個要點:
-
在使用SwipeRefreshLayout時,建議只包裹一個List類型的控件即可,代碼如下:
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id="@+id/srl_head" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rl_head" android:layout_width="match_parent" android:layout_height="match_parent" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
-
在實現SwipeRefreshLayout的監聽器
onRefresh()
方法後,下拉刷新時會循環展示刷新的動畫,需要在數據顯示完畢後手動調用setRefreshing(false)
來關閉動畫
4.1.1.4 RecyclerView
RecyclerView是Google官方提供的一種實現數據列表的控件,作爲替換ListView的數據列表控件,樣式和使用都要相對好一些。(可能是作者使用ListView比較久了,覺得RecyclerView的佈局適配比較難實現)
使用RecyclerView時,需要注意幾個要點:
- RecyclerView的適配器需要繼承
RecyclerView.Adapter<NewsDetailAdapter.ViewHolder>
,可以實現默認的ViewHolder優化; - RecyclerView的適配器的構造方法所接受的數據集合只有發生變動了,RecyclerView的數據刷新方法纔會生效,因此若單獨寫了其適配器類,則需要在獲取到數據的時候配置RecyclerView以及其適配器;
4.1.1.5 TabLayout
TabLayout是Google官方提供的一種實現導航欄的控件,作爲替換ViewPagerIndicator的導航欄,樣式和使用都要相對好一些。一般使用TabLayout都是需要搭配ViewPager的,因此可以使用官方提供的setupWithViewPager
來綁定TabLayout和ViewPager,並且在ViewPager註冊適配器時重寫getPageTitle
方法,以此來獲取由ViewPager所管理着的Fragment所對應的碎片(如果想要保證順序一致則需要在創建集合時調整插入的數據)
4.1.2 Gson的解析
4.1.2.1 Gson在遇到類型錯誤時的處理
Gson是Google官網提供用來解析Json數據的工具,只需要將Json數據轉化成實體類,就可以通過Gson將其轉化成對象的形式。
然而,有一種情況——平常解析的Json數據中的某個字段平時是一個對象類型,而在網絡不佳的情況下則會傳回一個空字符串(""),也就是說Json數據同一個字段同時出現了兩種類型的情況。在這樣的情形下,Gson會解析失敗,並且會直接拋出異常,導致App閃退。
爲了解決這個問題,Gson提供了JsonDeserializer接口來讓某個類自定義其反序列化的過程,具體操作可參照此篇博客:
JsonDeserializer——Gson自定義解析類型錯誤的字段
這篇博客很好地總結了Gson在解析Json同字段不同類型時的對策,事實上Gson還有許多使用方法,等待我們去學習。
4.1.2.2 Gson和緩存
爲了避免Gson在解析失敗等問題上拋出異常導致整個應用崩潰,可以在每次使用Gson解析數據之後將數據寫入緩存(文件、Sp和數據庫均可),這樣可以保證在異常情況下(沒有網絡,Gson解析失效,網絡傳輸慢)依然可以獲得之前解析好的數據。作者使用Sp作爲讀寫緩存,實現了一個簡單的緩存工具類jsonCache,代碼如下:
public class jsonCache {
// 設置緩存
public static void setCache(Context context, String key,String value) {
SharedPreferencesUtils.putString(context,key,value);
}
// 讀取緩存
public static String getCache(Context context, String key,String defvalue) {
String string = SharedPreferencesUtils.getString(context, key, defvalue);
return string;
}
}
4.1.3 ViewPager的懶加載
使用ViewPager來管理Fragment,會同時導致這些Fragment執行onCreateView
,即提前加載好所有的數據,這會讓應用接收龐大的數據導致卡頓。爲了解決這個問題,需要實現ViewPager的懶加載,即切換到這個Fragment時再獲取其數據,保證應用的流暢度。說來慚愧,這項優化其實作者還沒有進行落實,還在研究當中,力求尋找最優的方法,感興趣的讀者也可以查詢相應資料。
4.1.4 Glide的佔位圖
Glide是比較常用的圖片加載工具,底部封裝了三級緩存等大量圖片加載優化。Glide還提供了大量的工具方法,其中包括有佔位圖的設置,即在圖片還未加載出來時先放置佔位圖,這樣可以提高用戶的體驗度,提高了應用的可用性。
4.2 Web App(WebApp)
WebbApp,即使用Html、Css、JavaScript等前端語言進行實現的Android應用。中間因爲學習了一段時間的Java服務器的開發,自然而然也會接觸到這些前端語言的學習。由於技術棧的原因,可以讓熟悉前端的人在不熟悉後端語言的基礎上,開始Android應用的研發。
WebApp的開發相對其他兩種App開發模式要更爲迅速,因爲所有代碼基本上都是基於前端代碼來實現,並且WebApp對應的並非只有Android一個平臺,還支持IOS、Web等平臺。但與此同時,WebApp對於Android系統底層驅動的調用略顯乏力,尤其是不支持訪問本地數據庫的特性使其不支持作爲主流App的開發方向。另外WebApp的大部分功能都需要依據網絡,若失去網絡的支持,WebApp可能只形如空殼,很多開發者會戲稱WebApp爲“手機上的PPT”。
當然,作爲能夠快速開發並且UI設計較佳的開發模式,WebApp適合作爲資訊類等App的開發模式。WebApp仰仗於前端編程語言,具有控件豐富的組件市場,這也算是WebApp相較於其他開發模式較爲優勢的地方。
在開發WebApp時,使用的主要技術棧爲Uniapp,其主體語言爲Vue.js,若熟悉此技術棧會很快上手其開發。除了一些ES6語法之外,Uniapp也支持Scss等樣式,生態圈也較爲成熟,基本上可以做到大部分組件“拿來即用”,所以大致上沒有遇到什麼難點,就暫且略過這部分了。
4.3 HyBird App(混合App)
混合App,即混合使用幾種語言進行實現的Android應用,最典型的混合App技術棧就是React Native和Flutter。在學習了一段時間的原生App後,爲了讓App能夠同時支持Android和IOS端,避免一種App因爲平臺的不同而需要開發兩套項目的成本花銷,混合App應運而生。混合App擁有原生App的性能快和兼容性強等特性,還擁有WebApp的跨平臺運行和界面優美的特點,更像是這兩者App取長補短之後的產物。
當然,作爲Android開發方向的嶄新產物,混合App的生態圈還尚未完備,一些細節性的東西可能還是沒有原生App的實現要好,而兼容性方面或許還是WebApp要更勝一籌。由於React Native的配置需要使用npm,WebPack等前端工具來配置,步驟較爲繁瑣,所以這裏還是採用較新的Flutter來完成混合App的開發。
Flutter主要採用了Dart語言進行開發,Dart語言有點類似於Java語言,如果對Java比較熟悉的話會很快上手Dart語言。Flutter提供了比較方便的MaterialApp佈局,可以快速實現一個標準App的大概樣式。
接下來,談談作者在進行混合App開發時遇到的一些主要難點:
4.3.1 使用setState()刷新界面
在進行操作時,若界面上沒有顯示出來對應的數據,多半是沒有調用setState()來進行刷新,這是由於Flutter的特性所致。例如在點擊底部導航欄時,由於數據沒有發生變化,界面同樣也不會發生變化,就不會產生界面切換的效果,這時候就需要調用動態調用setState(),來通知這個Widget狀態已經發生了改變,需要重繪界面。
4.3.2 使用FlutterJsonBeanFactory來解析Json數據
在Flutter中,由於沒有FastJson、JackSon、Gson等Json解析工具,解析Json變得異常麻煩。使用Flutter原生的Convert雖然也可以解析Json,但是遇到格式複雜的Json數據時,實現龐大的Json實體類是很痛苦的事情。這時候就可以使用FlutterJsonBeanFactory來進行Json數據的解析,並自動生成實體類。在獲取數據時,只需要像使用Json解析工具時一樣即可,代碼如下:
// 使用FlutterJsonBeanFactory進行解析
Map jsonMap = json.decode(response.toString());
NewsEntity newsEntity = newsEntityFromJson(new NewsEntity(),jsonMap);
NewsResult result = newsEntity.result;
_newsDataList = result.xList;
4.3.3 使用Future完成異步操作
由於獲取網絡數據並解析之後放入列表中是耗時操作,需要將該操作放入異步模型中執行,防止阻塞主線程。Flutter的異步操作主要通過Future、Async、Await來實現,在獲取數據時,調用Future.builder()
,既可以獲取對應Future方法中的數據,並且可以監聽數據獲得的實時性,在未獲取到數據時播放環形進度條,代碼如下:
FutureBuilder<List<NewsResultList>>(
future: fetchNews(newsType),
builder: (context, snapshot){
if(snapshot.hasError) print(snapshot.error);
return snapshot.hasData ? NewsListItem(news: snapshot.data,scrollController: _scrollController) : Center(child: CircularProgressIndicator());
},
4.3.4 使用compute()完成線程隔離
儘管使用了Future來完成異步操作,在數據讀取的同時還是會導致主界面卡頓,這將會降低應用的使用感,Flutter提供了computer()來實現將操作的線程進行隔離的操作,與之對應的還有isolate
。但是isolate要相對重量級一些,這裏使用computer()即可達到目標。
5.總結
經過一週對三種App開發模式的學習並實踐,作爲Android開發人員,作者認爲原生App開發的基礎還是必要的,其中涉及了許多高深的原理需要去理解。如果對前端語言有些基礎的話,可以嘗試WebApp。而對於混合App,由於需要一定的學習成本,建議對原生App的開發相當熟悉之後再去嘗試,不然會對其中的許多概念感到模糊。