天天看點

一個星期使用三種不同的開發模式完成資訊類App——《聽風資訊》

文章目錄

  • ​​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)
一個星期使用三種不同的開發模式完成資訊類App——《聽風資訊》
一個星期使用三種不同的開發模式完成資訊類App——《聽風資訊》
  • Web App(WebApp)
  • 一個星期使用三種不同的開發模式完成資訊類App——《聽風資訊》
    一個星期使用三種不同的開發模式完成資訊類App——《聽風資訊》
  • HyBird App(混合App)
  • 一個星期使用三種不同的開發模式完成資訊類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​

​,如圖所示:

一個星期使用三種不同的開發模式完成資訊類App——《聽風資訊》

最開始做項目時,要實作底部标簽需要使用RadioGroup + RadioButton來實作這部分的功能,RadioButton還需要編寫一個Selector來滿足圖示在點選時顯示不同的圖樣,而使用BottomNavigationView似乎就能很好地解決這塊的問題。

當然,使用BottomNavigationView時,需要注意幾個要點:

  1. 在初始化BottomNavigationView管理着的Fragment時,如果你的項目中使用到了Toolbar,則需要先綁定Toolbar,否則會報空指針異常,代碼如下:
// 初始化ToolBar,注意要在Fragment初始化之前調用,不然會報空指針異常,這裡踩過坑!
setSupportActionBar(tb_title);
// 初始化Fragment
initFragment();      
  1. id的對應。​

    ​<menu>​

    ​标簽和​

    ​<item>​

    ​标簽中控件的id要對應,不然會不顯示内容。
  2. 另外,若使用BottomNavigationView,布局則推薦使用ConstraintLayout,即限制布局,這樣會比較好控制控件的擺放(這個控件作者本人用的也不是很熟練,在使用時遇到了BottomNavigationView遮擋RecyclerView的情況,導緻内容顯示不全,最後作者用了很笨的方法才調整成功,希望有比較了解這塊内容的讀者能夠在評論區不吝賜教,作者将感激不盡)
4.1.1.2 Toolbar

Toolbar是Google官方提供的一種實作狀态欄切換的控件,作為替換Actionbar的狀态欄,功能要更為強大。

使用Toolbar時,需要注意幾個要點:

  1. 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>      
  1. 要通過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>      
  1. 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);
    }
    }
}      
  1. 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);
}      
  1. 可以在設定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時,需要注意幾個要點:

  1. 在使用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>      
  1. 在實作SwipeRefreshLayout的監聽器​

    ​onRefresh()​

    ​方法後,下拉重新整理時會循環展示重新整理的動畫,需要在資料顯示完畢後手動調用​

    ​setRefreshing(false)​

    ​來關閉動畫
4.1.1.4 RecyclerView

RecyclerView是Google官方提供的一種實作資料清單的控件,作為替換ListView的資料清單控件,樣式和使用都要相對好一些。(可能是作者使用ListView比較久了,覺得RecyclerView的布局适配比較難實作)

使用RecyclerView時,需要注意幾個要點:

  1. RecyclerView的擴充卡需要繼承​

    ​RecyclerView.Adapter<NewsDetailAdapter.ViewHolder>​

    ​,可以實作預設的ViewHolder優化;
  2. 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());
              },      

4.3.4 使用compute()完成線程隔離

5.總結