文章目錄
- 1.引言
- 2.App開發模式的主要差別
- 3.App開發模式在開發項目時所使用到的技術棧
- 4.App開發時的感想
- 4.1 Native App(原生App)
- 4.1.1 Material Design的設計
- 4.1.1.1 BottomNavigationView
- 4.1.1.2 Toolbar
- 4.1.1.3 SwipeRefreshLayout
- 4.1.1.4 RecyclerView
- 4.1.1.5 TabLayout
- 4.1.2 Gson的解析
- 4.1.2.1 Gson在遇到類型錯誤時的處理
- 4.1.2.2 Gson和緩存
- 4.1.3 ViewPager的懶加載
- 4.1.4 Glide的占位圖
- 4.2 Web App(WebApp)
- 4.3 HyBird App(混合App)
- 4.3.1 使用setState()重新整理界面
- 4.3.2 使用FlutterJsonBeanFactory來解析Json資料
- 4.3.3 使用Future完成異步操作
- 4.3.4 使用compute()完成線程隔離
- 5.總結
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>
标簽中控件的id要對應,不然會不顯示内容。<item>
- 另外,若使用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">"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時用
來界定其高度,代表之前應用還擁有Actionbr控件時的高度,控件整體代碼如下:?attr/actionBarSize
<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的擴充卡需要繼承
,可以實作預設的ViewHolder優化;RecyclerView.Adapter<NewsDetailAdapter.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方法中的資料,并且可以監聽資料獲得的實時性,在未擷取到資料時播放環形進度條,代碼如下:
<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());
},