前言:茍有恒,何必三更眠五更起;最無(wú)益,莫過(guò)一日曝十日寒。
前言
之前一直想寫個(gè) WanAndroid 項(xiàng)目來(lái)鞏固自己對(duì) Kotlin+Jetpack+協(xié)程
等知識(shí)的學(xué)習(xí),但是一直沒(méi)有時(shí)間。這里重新行動(dòng)起來(lái),從項(xiàng)目搭建到完成前前后后用了兩個(gè)月時(shí)間,平常時(shí)間比較少,基本上都是只能利用零碎的時(shí)間來(lái)寫。但不再是想寫一個(gè)簡(jiǎn)單的玩安卓項(xiàng)目,我從多個(gè)大型項(xiàng)目中學(xué)習(xí)和吸取經(jīng)驗(yàn),從0到1打造一個(gè)符合大型項(xiàng)目的架構(gòu)模式。
這或許是一個(gè)縮影,但是麻雀雖小,五臟俱全,這肯定能給大家?guī)?lái)一些想法和思考。當(dāng)然這個(gè)項(xiàng)目的功能并未全部完善,因?yàn)槲覀兊哪康牟皇窃煲粋€(gè) WanAndroid 客戶端,而是學(xué)習(xí)搭建和使用 Kotlin+協(xié)程+Flow+Retrofit+Jetpack+MVVM+組件化+模塊化+短視頻 這一種架構(gòu),更好的提升自己。后續(xù)我也會(huì)不斷完善和優(yōu)化,在保證擁有一個(gè)正常的 APP 功能之外,繼續(xù)加入 Compose
,依賴注入Hint
,性能優(yōu)化
,MVI模式
,支付功能
等的實(shí)踐。
一、項(xiàng)目簡(jiǎn)介
- 項(xiàng)目采用 Kotlin 語(yǔ)言編寫,結(jié)合 Jetpack 相關(guān)控件,
Navigation
,Lifecyle
,DataBinding
,LiveData
,ViewModel
等搭建的 MVVM 架構(gòu)模式; - 通過(guò)組件化,模塊化拆分,實(shí)現(xiàn)項(xiàng)目更好解耦和復(fù)用,ARouter 實(shí)現(xiàn)模塊間通信;
- 使用 協(xié)程+Flow+Retrofit+OkHttp 優(yōu)雅地實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求;
- 通過(guò)
mmkv
,Room
數(shù)據(jù)庫(kù)等實(shí)現(xiàn)對(duì)數(shù)據(jù)緩存的管理; - 使用谷歌
ExoPlayer
實(shí)現(xiàn)短視頻播放; - 使用 Glide 完成圖片加載;
- 通過(guò) WanAndroid 提供的 API 實(shí)現(xiàn)的一款玩安卓客戶端。
項(xiàng)目使用MVVM架構(gòu)模式,基本上遵循 Google 推薦的架構(gòu),對(duì)于 Repository
,Google 認(rèn)為 ViewModel
僅僅用來(lái)做數(shù)據(jù)的存儲(chǔ),數(shù)據(jù)加載應(yīng)該由 Repository
來(lái)完成。通過(guò) Room 數(shù)據(jù)庫(kù)實(shí)現(xiàn)對(duì)數(shù)據(jù)的緩存,在無(wú)網(wǎng)絡(luò)或者弱網(wǎng)的情況下優(yōu)先展示緩存數(shù)據(jù)。
項(xiàng)目截圖:








項(xiàng)目地址: https://github.com/suming77/SumTea_Android
二、項(xiàng)目詳情
2.1 基礎(chǔ)架構(gòu)
(1) BaseActicity
通過(guò)單一職責(zé)原則,實(shí)現(xiàn)職能分級(jí),使用者只需要按需繼承即可。
- BaseActivity:?????封裝了通用的 init 方法,初始化布局,加載彈框等方法,提供了原始的添加布局的方式;
-
BaseDataBindActivity:繼承自
BaseActivity
,通過(guò) dataBinding 綁定布局,利用泛型參數(shù)反射創(chuàng)建布局文件實(shí)例,獲取布局 view,不再需要findViewById()
;
val type = javaClass.genericSuperclass
val vbClass: Class<DB> = type!!.saveAs<ParameterizedType>().actualTypeArguments[0].saveAs()
val method = vbClass.getDeclaredMethod("inflate", LayoutInflater::class.java)
mBinding = method.invoke(this, layoutInflater)!!.saveAsUnChecked()
setContentView(mBinding.root)
-
BaseMvvmActivity:?繼承自
BaseDataBindActivity
,通過(guò)泛型參數(shù)反射自動(dòng)創(chuàng)建ViewModel
實(shí)例,更方便使用ViewModel
實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求。
val argument = (this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
mViewModel = ViewModelProvider(this).get(argument[1] as Class<VM>)
(2) BaseFragment
BaseFragment 的封裝與上面的 BaseActivity 類似。
(3) BaseRecyclerViewAdapter
-
BaseRecyclerViewAdapter:封裝了
RecyclerViewAdapter
基類,實(shí)現(xiàn)提供創(chuàng)建ViewHolder
能力,提供添加頭尾布局能力,通用的 Item 點(diǎn)擊事件,提供 dataBinding 能力,不再需要findViewById()
,提供了多種刷新數(shù)據(jù)的方式,全局刷新,局部刷新等等。 -
BaseMultiItemAdapter:??提供了實(shí)現(xiàn)多種不同布局的 Adapter,根據(jù)不同的 ViewType 實(shí)現(xiàn)不同的
ViewBinding
,再創(chuàng)建返回不同的ViewHolder
。
(4) Ext拓展類
項(xiàng)目中提供了大量控件擴(kuò)展類,能夠快速開(kāi)發(fā),提高效率:
- ResourceExt:??資源文件擴(kuò)展類;
- TextViewExt:??TextView 擴(kuò)展類;
- SpanExt:????Span 拓展類,實(shí)現(xiàn)多種 Span 效果;
- RecyclerViewExt:一行代碼快速實(shí)現(xiàn)添加垂直分割線,網(wǎng)格分割線;
- ViewExt:????View 擴(kuò)展類,實(shí)現(xiàn)點(diǎn)擊防抖,添加間距,設(shè)置寬度,設(shè)置可見(jiàn)性等等;
-
EditTextExt:??通過(guò) Flow 構(gòu)建輸入框文字變化流,
filter{}
實(shí)現(xiàn)數(shù)據(jù)過(guò)濾,避免無(wú)效請(qǐng)求,debounce()
實(shí)現(xiàn)防抖; - GsonExt:????一行代碼快速實(shí)現(xiàn) Bean 和 Json 之間的相互轉(zhuǎn)換。
//將Bean對(duì)象轉(zhuǎn)換成json字符串
fun Any.toJson(includeNulls: Boolean = true): String {
return gson(includeNulls).toJson(this)
}
//將json字符串轉(zhuǎn)換成目標(biāo)Bean對(duì)象
inline fun <reified T> String.toBean(includeNulls: Boolean = true): T {
return gson(includeNulls).fromJson(this, object : TypeToken<T>() {}.type)
}
(5) xlog
XLog 是一個(gè)高性能文本存儲(chǔ)方案,在真實(shí)環(huán)境中經(jīng)受了微信數(shù)億級(jí)別的考驗(yàn),具有很好的穩(wěn)定性。由于其是使用C語(yǔ)言來(lái)實(shí)現(xiàn)的,故有占用性能、內(nèi)存小,存儲(chǔ)速度快等優(yōu)點(diǎn),支持多線程,甚至多進(jìn)程的使用,支持定期刪除日志,同時(shí),擁有特定算法,進(jìn)行了文件的壓縮,甚至可以配置文件加密。
利用 Xlog 建設(shè)客戶端運(yùn)行時(shí)日志體系,遠(yuǎn)程日志按需回?fù)?,以打點(diǎn)的形式記錄關(guān)鍵執(zhí)行流程。
2.2 Jetpack組件
Android Jetpack是一組 Android 軟件組件、工具和指南,它們可以幫助開(kāi)發(fā)者構(gòu)建高質(zhì)量、穩(wěn)定的 Android 應(yīng)用程序。Jetpack 中包含多個(gè)庫(kù),它們旨在解決 Android 應(yīng)用程序開(kāi)發(fā)中的常見(jiàn)問(wèn)題,并提供一致的 API 和開(kāi)發(fā)體驗(yàn)。
項(xiàng)目中僅僅使用到上圖的一小部分組件。
(1) Navtgation
Navtgation 作為構(gòu)建應(yīng)用內(nèi)界面的框架,重點(diǎn)是讓單 Activity 應(yīng)用成為首選架構(gòu)(一個(gè)應(yīng)用只需一個(gè) Activity),它的定位是頁(yè)面路由。
項(xiàng)目中主頁(yè)分為5個(gè) Tab,主要為首頁(yè)、分類、體系、我的。使用 BottomNavigationView
+ Navigation
來(lái)搭建。通過(guò) menu 來(lái)配置底部菜單,通過(guò) NavHostFragment
來(lái)配置各個(gè) Fragment。同時(shí)解決了 Navigation
與 BottomNavigationView
結(jié)合使用時(shí),點(diǎn)擊 tab,F(xiàn)ragment 每次都會(huì)重新創(chuàng)建問(wèn)題。解決方法是自定義 FragmentNavigator
,將內(nèi)部 replace()
替換為 show()/hide()
。
(2) ViewBinding&DataBinding
-
ViewBinding
的出現(xiàn)就是不再需要寫findViewById()
; -
DataBinding
是一種工具,它解決了 View 和數(shù)據(jù)之間的雙向綁定;減少代碼模板,不再需要寫findViewById()
;釋放 Activity/Fragment,可以在 XML 中完成數(shù)據(jù),事件綁定工作,讓Activity/Fragment
更加關(guān)心核心業(yè)務(wù);數(shù)據(jù)綁定空安全,在 XML 中綁定數(shù)據(jù)它是空安全的,因?yàn)?DataBinding
在數(shù)據(jù)綁定上會(huì)自動(dòng)裝箱和空判斷,所以大大減少了 NPE 問(wèn)題。
(3) ViewModel
ViewModel
具備生命感知能力的數(shù)據(jù)存儲(chǔ)組件。頁(yè)面配置更改數(shù)據(jù)不會(huì)丟失,數(shù)據(jù)共享(單 Activity 多 Fragment 場(chǎng)景下的數(shù)據(jù)共享),以生命周期的方式管理界面相關(guān)的數(shù)據(jù),通常和 DataBinding 配合使用,為實(shí)現(xiàn) MVVM 架構(gòu)提供了強(qiáng)有力的支持。
(4) LiveData
LiveData
是一個(gè)具有生命周期感知能力的數(shù)據(jù)訂閱,分發(fā)組件。支持共享資源(一個(gè)數(shù)據(jù)支持被多個(gè)觀察者接收的),支持粘性事件的分發(fā),不再需要手動(dòng)處理生命周期(和宿主生命周期自動(dòng)關(guān)聯(lián)),確保界面符合數(shù)據(jù)狀態(tài)。在底層數(shù)據(jù)庫(kù)更改時(shí)通知 View。
(5) Room
一個(gè)輕量級(jí) orm 數(shù)據(jù)庫(kù),本質(zhì)上是一個(gè) SQLite 抽象層。使用更加簡(jiǎn)單(Builder 模式,類似 Retrofit),通過(guò)注解的形式實(shí)現(xiàn)相關(guān)功能,編譯時(shí)自動(dòng)生成實(shí)現(xiàn)類 IMPL。
這里主要用于首頁(yè)視頻列表緩存數(shù)據(jù),與 LiveData
和 Flow 結(jié)合處理可以避免不必要的 NPE,可以監(jiān)聽(tīng)數(shù)據(jù)庫(kù)表中的數(shù)據(jù)的變化,也可以和 RXJava 的 Observer 使用,一旦發(fā)生了 insert,update,delete等操作,Room 會(huì)自動(dòng)讀取表中最新的數(shù)據(jù),發(fā)送給 UI 層,刷新頁(yè)面。
Room 庫(kù)架構(gòu)的示意圖:
Room 包含三個(gè)主要組件:
- 數(shù)據(jù)庫(kù)類:用于保存數(shù)據(jù)庫(kù)并作為應(yīng)用持久性數(shù)據(jù)底層連接的主要訪問(wèn)點(diǎn);
- 數(shù)據(jù)實(shí)體:用于表示應(yīng)用的數(shù)據(jù)庫(kù)中的表;
- 數(shù)據(jù)訪問(wèn)對(duì)象 (DAO):提供您的應(yīng)用可用于查詢、更新、插入和刪除數(shù)據(jù)庫(kù)中的數(shù)據(jù)的方法。
Dao
@Dao
interface VideoListCacheDao {
//插入單個(gè)數(shù)據(jù)
@Insert(entity = VideoInfo::class, onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(videoInfo: VideoInfo)
//插入多個(gè)數(shù)據(jù)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(videoList: MutableList<VideoInfo>)
//刪除指定item 使用主鍵將傳遞的實(shí)體實(shí)例與數(shù)據(jù)庫(kù)中的行進(jìn)行匹配。如果沒(méi)有具有相同主鍵的行,則不會(huì)進(jìn)行任何更改
@Delete
fun delete(videoInfo: VideoInfo): Int
//刪除表中所有數(shù)據(jù)
@Query("DELETE FROM $TABLE_VIDEO_LIST")
suspend fun deleteAll()
//更新某個(gè)item,不指定的entity也可以,會(huì)根據(jù)你傳入的參數(shù)對(duì)象來(lái)找到你要操作的那張表
@Update
fun update(videoInfo: VideoInfo): Int
//根據(jù)id更新數(shù)據(jù)
@Query("UPDATE $TABLE_VIDEO_LIST SET title=:title WHERE id=:id")
fun updateById(id: Long, title: String)
//查詢所有數(shù)據(jù)
@Query("SELECT * FROM $TABLE_VIDEO_LIST")
fun queryAll(): MutableList<VideoInfo>?
//根據(jù)id查詢某個(gè)數(shù)據(jù)
@Query("SELECT * FROM $TABLE_VIDEO_LIST WHERE id=:id")
fun query(id: Long): VideoInfo?
//通過(guò)LiveData以觀察者的形式獲取數(shù)據(jù)庫(kù)數(shù)據(jù),可以避免不必要的NPE
@Query("SELECT * FROM $TABLE_VIDEO_LIST")
fun queryAllLiveData(): LiveData<List<VideoInfo>>
}
Database
@Database(entities = [VideoInfo::class], version = 1, exportSchema = false)
abstract class SumDataBase : RoomDatabase() {
//抽象方法或者抽象類標(biāo)記
abstract fun videoListDao(): VideoListCacheDao
companion object {
private var dataBase: SumDataBase? = null
//同步鎖,可能在多個(gè)線程中同時(shí)調(diào)用
@Synchronized
fun getInstance(): SumDataBase {
return dataBase ?: Room.databaseBuilder(SumAppHelper.getApplication(), SumDataBase::class.java, "SumTea_DB")
//是否允許在主線程查詢,默認(rèn)是false
.allowMainThreadQueries()
.build()
}
}
}
注意:Room 數(shù)據(jù)庫(kù)中的 Dao 中定義數(shù)據(jù)庫(kù)操作的方法一定要確保用法正確,否則會(huì)導(dǎo)致 Room 編譯時(shí)生成的實(shí)現(xiàn)類錯(cuò)誤,編譯不通過(guò)等問(wèn)題。
2.3 網(wǎng)絡(luò)請(qǐng)求庫(kù)
項(xiàng)目的網(wǎng)絡(luò)請(qǐng)求封裝提供了兩種方式的實(shí)現(xiàn),一種是協(xié)程+Retrofit+ViewModel+Repository,像官網(wǎng)那樣加一層 Repository
去管理網(wǎng)絡(luò)請(qǐng)求調(diào)用;另一種方式是通過(guò) Flow 流配合 Retrofit 更優(yōu)雅實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求,對(duì)比官網(wǎng)的做法更加簡(jiǎn)潔。
(1) Retrofit+協(xié)程+Repository
BaseViewModel
open class BaseViewModel : ViewModel() {
//需要運(yùn)行在協(xié)程作用域中
suspend fun <T> safeApiCall(
errorBlock: suspend (Int?, String?) -> Unit,
responseBlock: suspend () -> T?
): T? {
try {
return responseBlock()
} catch (e: Exception) {
e.printStackTrace()
LogUtil.e(e)
val exception = ExceptionHandler.handleException(e)
errorBlock(exception.errCode, exception.errMsg)
}
return null
}
}
BaseRepository
open class BaseRepository {
//IO中處理請(qǐng)求
suspend fun <T> requestResponse(requestCall: suspend () -> BaseResponse<T>?): T? {
val response = withContext(Dispatchers.IO) {
withTimeout(10 * 1000) {
requestCall()
}
} ?: return null
if (response.isFailed()) {
throw ApiException(response.errorCode, response.errorMsg)
}
return response.data
}
}
HomeRepository的使用
class HomeRepository : BaseRepository() {
//項(xiàng)目tab
suspend fun getProjectTab(): MutableList<ProjectTabItem>? {
return requestResponse {
ApiManager.api.getProjectTab()
}
}
}
HomeViewModel的使用
class HomeViewModel : BaseViewModel() {
//請(qǐng)求項(xiàng)目Tab數(shù)據(jù)
fun getProjectTab(): LiveData<MutableList<ProjectTabItem>?> {
return liveData {
val response = safeApiCall(errorBlock = { code, errorMsg ->
TipsToast.showTips(errorMsg)
}) {
homeRepository.getProjectTab()
}
emit(response)
}
}
}
(2) Flow優(yōu)雅實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求
Flow 其實(shí)和 RxJava 很像,非常方便,用它來(lái)做網(wǎng)絡(luò)請(qǐng)求更加簡(jiǎn)潔。
suspend fun <T> requestFlowResponse(
errorBlock: ((Int?, String?) -> Unit)? = null,
requestCall: suspend () -> BaseResponse<T>?,
showLoading: ((Boolean) -> Unit)? = null
): T? {
var data: T? = null
//1.執(zhí)行請(qǐng)求
flow {
//設(shè)置超時(shí)時(shí)間
val response = requestCall()
if (response?.isFailed() == true) {
errorBlock.invoke(response.errorCode, response.errorMsg)
}
//2.發(fā)送網(wǎng)絡(luò)請(qǐng)求結(jié)果回調(diào)
emit(response)
//3.指定運(yùn)行的線程,flow {}執(zhí)行的線程
}.flowOn(Dispatchers.IO)
.onStart {
//4.請(qǐng)求開(kāi)始,展示加載框
showLoading?.invoke(true)
}
//5.捕獲異常
.catch { e ->
e.printStackTrace()
LogUtil.e(e)
val exception = ExceptionHandler.handleException(e)
errorBlock?.invoke(exception.errCode, exception.errMsg)
}
//6.請(qǐng)求完成,包括成功和失敗
.onCompletion {
showLoading?.invoke(false)
//7.調(diào)用collect獲取emit()回調(diào)的結(jié)果,就是請(qǐng)求最后的結(jié)果
}.collect {
data = it?.data
}
return data
}
2.4 圖片加載庫(kù)
Glide
圖片加載利用 Glide 進(jìn)行了簡(jiǎn)單的封裝,對(duì) ImageView 做擴(kuò)展函數(shù)處理:
//加載圖片,開(kāi)啟緩存
fun ImageView.setUrl(url: String?) {
if (ActivityManager.isActivityDestroy(context)) {
return
}
Glide.with(context).load(url)
.placeholder(R.mipmap.default_img) // 占位符,異常時(shí)顯示的圖片
.error(R.mipmap.default_img) // 錯(cuò)誤時(shí)顯示的圖片
.skipMemoryCache(false) //啟用內(nèi)存緩存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) //磁盤緩存策略
.into(this)
}
//加載圓形圖片
fun ImageView.setUrlCircle(url: String?) {
if (ActivityManager.isActivityDestroy(context)) return
Glide.with(context).load(url)
.placeholder(R.mipmap.default_head)
.error(R.mipmap.default_head)
.skipMemoryCache(false) //啟用內(nèi)存緩存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(CenterCrop()) // 圓形
.into(this)
}
//加載圓角圖片
fun ImageView.setUrlRound(url: String?, radius: Int = 10) {
if (ActivityManager.isActivityDestroy(context)) return
Glide.with(context).load(url)
.placeholder(R.mipmap.default_img)
.error(R.mipmap.default_img)
.skipMemoryCache(false) // 啟用內(nèi)存緩存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(CenterCrop(), RoundedCorners(radius))
.into(this)
}
//加載Gif圖片
fun ImageView.setUrlGif(url: String?) {
if (ActivityManager.isActivityDestroy(context)) return
Glide.with(context).asGif().load(url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.placeholder(R.mipmap.default_img)
.error(R.mipmap.default_img)
.into(this)
}
/**
* 設(shè)置圖片高斯模糊
* @param radius 設(shè)置模糊度(在0.0到25.0之間),默認(rèn)25
* @param sampling 圖片縮放比例,默認(rèn)1
*/
fun ImageView.setBlurView(url: String?, radius: Int = 25, sampling: Int = 1) {
if (ActivityManager.isActivityDestroy(context)) return
//請(qǐng)求配置
val options = RequestOptions.bitmapTransform(BlurTransformation(radius, sampling))
Glide.with(context)
.load(url)
.placeholder(R.mipmap.default_img)
.error(R.mipmap.default_img)
.apply(options)
.into(this)
}
- 修復(fù) Glide 的圖片裁剪和 ImageView 的
scaleType
的沖突問(wèn)題,Bitmap 會(huì)先圓角裁剪,再加載到 ImageView 中,如果 Bitmap 圖片尺寸大于 ImageView 尺寸,則會(huì)看不到,使用CenterCrop()
重載,會(huì)先將 Bitmap 居中裁剪,再進(jìn)行圓角處理,這樣就能看到。 - 提供了 GIF 圖加載和圖片高斯模糊效果功能。
2.5 WebView
我們都知道原生的 WebView 存在很多問(wèn)題,使用騰訊X5內(nèi)核 WebView 進(jìn)行封裝,兼容性,穩(wěn)定性,安全性,速度都有很大的提升。
項(xiàng)目中使用 WebView 展示文章詳情頁(yè)。
2.6 MMKV
MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化 / 反序列化使用 protobuf 實(shí)現(xiàn),性能高,穩(wěn)定性強(qiáng)。使用簡(jiǎn)單,支持多進(jìn)程。
在 App 啟動(dòng)時(shí)初始化 MMKV,設(shè)定 MMKV 的根目錄(files/mmkv/),例如在 Application
里:
public void onCreate() {
super.onCreate();
String rootDir = MMKV.initialize(this);
LogUtil.e("mmkv root: " + rootDir);
}
MMKV 提供一個(gè)全局的實(shí)例,可以直接使用:
import com.tencent.mmkv.MMKV;
//……
MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");
kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");
kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");
循環(huán)寫入隨機(jī)的 int
1k 次,有如下性能對(duì)比:
項(xiàng)目中使用 MMKV 保存用戶相關(guān)信息,包括用戶登錄 Cookies,用戶名稱,手機(jī)號(hào)碼,搜索歷史數(shù)據(jù)等信息。
2.7 ExoPlayer視頻播放器
ExoPlayer
是 google 推出的開(kāi)源播放器,主要是集成了 Android 提供的一套解碼系統(tǒng)來(lái)解析視頻和音頻,將 MediaCodec
封裝地非常完善,形成了一個(gè)性能優(yōu)越,播放穩(wěn)定性較好的一個(gè)開(kāi)發(fā)播放器,支持更多的視頻播放格式(包含 DASH 和 SmoothStreaming
,這2種 MediaPlayer
不支持),通過(guò)組件化自定義播放器,方便擴(kuò)展定制,持久的高速緩存,另外 ExoPlayer
包大小輕便,接入簡(jiǎn)單。
項(xiàng)目中使用 ExoPlayer
實(shí)現(xiàn)防抖音短視頻播放:
class VideoPlayActivity : BaseDataBindActivity<ActivityVideoPlayBinding>() {
//創(chuàng)建exoplayer播放器實(shí)例,視屏畫面渲染工廠類,語(yǔ)音選擇器,緩存控制器
private fun initPlayerView(): Boolean {
//創(chuàng)建exoplayer播放器實(shí)例
mPlayView = initStylePlayView()
// 創(chuàng)建 MediaSource 媒體資源 加載的工廠類
mMediaSource = ProgressiveMediaSource.Factory(buildCacheDataSource())
mExoPlayer = initExoPlayer()
//緩沖完成自動(dòng)播放
mExoPlayer?.playWhenReady = mStartAutoPlay
//將顯示控件綁定ExoPlayer
mPlayView?.player = mExoPlayer
//資源準(zhǔn)備,如果設(shè)置 setPlayWhenReady(true) 則資源準(zhǔn)備好就立馬播放。
mExoPlayer?.prepare()
return true
}
//初始化ExoPlayer
private fun initExoPlayer(): ExoPlayer {
val playerBuilder = ExoPlayer.Builder(this).setMediaSourceFactory(mMediaSource)
//視頻每一幀的畫面如何渲染,實(shí)現(xiàn)默認(rèn)的實(shí)現(xiàn)類
val renderersFactory: RenderersFactory = DefaultRenderersFactory(this)
playerBuilder.setRenderersFactory(renderersFactory)
//視頻的音視頻軌道如何加載,使用默認(rèn)的軌道選擇器
playerBuilder.setTrackSelector(DefaultTrackSelector(this))
//視頻緩存控制邏輯,使用默認(rèn)的即可
playerBuilder.setLoadControl(DefaultLoadControl())
return playerBuilder.build()
}
//創(chuàng)建exoplayer播放器實(shí)例
private fun initStylePlayView(): StyledPlayerView {
return StyledPlayerView(this).apply {
controllerShowTimeoutMs = 10000
setKeepContentOnPlayerReset(false)
setShowBuffering(SHOW_BUFFERING_NEVER)//不展示緩沖view
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
useController = false //是否使用默認(rèn)控制器,如需要可參考PlayerControlView
// keepScreenOn = true
}
}
//創(chuàng)建能夠 邊播放邊緩存的 本地資源加載和http網(wǎng)絡(luò)數(shù)據(jù)寫入的工廠類
private fun buildCacheDataSource(): DataSource.Factory {
//創(chuàng)建http視頻資源如何加載的工廠對(duì)象
val upstreamFactory = DefaultHttpDataSource.Factory()
//創(chuàng)建緩存,指定緩存位置,和緩存策略,為最近最少使用原則,最大為200m
mCache = SimpleCache(
application.cacheDir,
LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200),
StandaloneDatabaseProvider(this)
)
//把緩存對(duì)象cache和負(fù)責(zé)緩存數(shù)據(jù)讀取、寫入的工廠類CacheDataSinkFactory 相關(guān)聯(lián)
val cacheDataSinkFactory = CacheDataSink.Factory().setCache(mCache).setFragmentSize(Long.MAX_VALUE)
return CacheDataSource.Factory()
.setCache(mCache)
.setUpstreamDataSourceFactory(upstreamFactory)
.setCacheReadDataSourceFactory(FileDataSource.Factory())
.setCacheWriteDataSinkFactory(cacheDataSinkFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}
}
2.8 組件化&模塊化
組件化&模塊化有利于業(yè)務(wù)模塊分離,高內(nèi)聚,低耦合,代碼邊界清晰。有利于團(tuán)隊(duì)合作多線開(kāi)發(fā),加快編譯速度,提高開(kāi)發(fā)效率,管理更加方便,利于維護(hù)和迭代。
宿主 App 中只有一個(gè) Application,整個(gè)業(yè)務(wù)被拆分為各個(gè) mod 模塊和 lib 組件庫(kù)。對(duì)一些功能組件進(jìn)行封裝抽取為 lib,給上層提供依賴。mod 模塊之間沒(méi)有任務(wù)依賴關(guān)系,通過(guò) Arouter 進(jìn)行通信。
(1) 模塊化
項(xiàng)目中通過(guò)以業(yè)務(wù)為維度把 App 拆分成主頁(yè)模塊,登錄模塊,搜索模塊,用戶模塊,視頻模塊等,相互間不可以訪問(wèn)不可以作為依賴,與此同時(shí)他們共同依賴于基礎(chǔ)庫(kù),網(wǎng)絡(luò)請(qǐng)求庫(kù),公共資源庫(kù),圖片加載庫(kù)等。如果還需要使用到啟動(dòng)器組件、Banner組件、數(shù)據(jù)庫(kù)Room組件等則單獨(dú)按需添加。
APP 殼工程負(fù)責(zé)打包環(huán)境,簽名,混淆規(guī)則,業(yè)務(wù)模塊集成,APP 主題等配置等工作,一般不包含任何業(yè)務(wù)。
(2) 組件化
模塊化和組件化最明顯的區(qū)別就是模塊相對(duì)組件來(lái)說(shuō)粒度更大。一個(gè)模塊中可能包含多個(gè)組件。在劃分的時(shí)候,模塊化是業(yè)務(wù)導(dǎo)向,組件化是功能導(dǎo)向。組件化是建立在模塊化思想上的一次演進(jìn)。
項(xiàng)目中以功能維度拆分了啟動(dòng)器組件、Banner組件、數(shù)據(jù)庫(kù)Room組件等組件。模塊化&組件化拆分后工程圖:
(3) 組件間通信
組件化之后就無(wú)法直接訪問(wèn)其他模塊的類和方法,這是個(gè)比較突出的問(wèn)題,就像原來(lái)可以直接使用 LogintManager
來(lái)拉起登錄,判斷是否已登錄,但是這個(gè)類已經(jīng)被拆分到了 mod_login 模塊下,而業(yè)務(wù)模塊之間是不能互相作為依賴的,所以無(wú)法在其他模塊直接使用 LogintManager
。
主要借助阿里的路由框架 ARouter 實(shí)現(xiàn)組件間通信,把對(duì)外提供的能力,以接口的形式暴露出去。
比如在公共資源庫(kù)中的 service 包下創(chuàng)建 ILoginService
,提供對(duì)外暴露登錄的能力,在 mod_login 模塊中提供 LoginServiceImpl
實(shí)現(xiàn)類,任意模塊就可以通過(guò) LoginServiceProvider
使用 iLoginService
對(duì)外提供暴露的能力。
- 公共資源庫(kù)中創(chuàng)建
ILoginService
,提供對(duì)外暴露登錄的能力。
interface ILoginService : IProvider {
//是否登錄
fun isLogin(): Boolean
//跳轉(zhuǎn)登錄頁(yè)
fun login(context: Context)
//登出
fun logout(
context: Context,
lifecycleOwner: LifecycleOwner?,
observer: Observer<Boolean>
)
}
- mod_login 模塊中
LoginService
提供ILoginService
的具體實(shí)現(xiàn)。
@Route(path = LOGIN_SERVICE_LOGIN)
class LoginService : ILoginService {
//是否登錄
override fun isLogin(): Boolean {
return UserServiceProvider.isLogin()
}
//跳轉(zhuǎn)登錄頁(yè)
override fun login(context: Context) {
context.startActivity(Intent(context, LoginActivity::class.java))
}
//登出
override fun logout(
context: Context,
lifecycleOwner: LifecycleOwner?,
observer: Observer<Boolean>
) {
val scope = lifecycleOwner?.lifecycleScope ?: GlobalScope
scope.launch {
val response = ApiManager.api.logout()
if (response?.isFailed() == true) {
TipsToast.showTips(response.errorMsg)
return@launch
}
LogUtil.e("logout${response?.data}", tag = "smy")
observer.onChanged(response?.isFailed() == true)
login(context)
}
}
override fun init(context: Context?) {}
}
- 公共資源庫(kù)中創(chuàng)建
LoginServiceProvider
,獲取LoginService
,提供使用方法。
object LoginServiceProvider {
//獲取loginService實(shí)現(xiàn)類
val loginService = ARouter.getInstance().build(LOGIN_SERVICE_LOGIN).navigation() as? ILoginService
//是否登錄
fun isLogin(): Boolean {
return loginService.isLogin()
}
//跳轉(zhuǎn)登錄
fun login(context: Context) {
loginService.login(context)
}
//登出
fun logout(
context: Context,
lifecycleOwner: LifecycleOwner?,
observer: Observer<Boolean>
) {
loginService.logout(context, lifecycleOwner, observer)
}
}
那么其他模塊就可以通過(guò) LoginServiceProvider
使用 iLoginService
對(duì)外提供暴露的能力。雖然看起來(lái)這么做會(huì)顯得更復(fù)雜,單一工程可能更加適合我們,每個(gè)類都能直接訪問(wèn),每個(gè)方法都能直接調(diào)用,但是我們不能局限于單人開(kāi)發(fā)的環(huán)境,在實(shí)際場(chǎng)景上多人協(xié)作是常態(tài),模塊化開(kāi)發(fā)是主流。
(4) Module單獨(dú)運(yùn)行
使得模塊可以在集成和獨(dú)立調(diào)試之間切換特性。在打包時(shí)是 library,在調(diào)試是 application。
- 在
config.gradle
文件中加入isModule
參數(shù):
//是否單獨(dú)運(yùn)行某個(gè)module
isModule = false
- 在每個(gè)
Module
的build.gradle
中加入isModule
的判斷,以區(qū)分是 application 還是 library:
// 組件模式和基礎(chǔ)模式切換
def root = rootProject.ext
if (root.isModule) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
sourceSets {
main {
if (rootProject.ext.isModule) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//library模式下排除debug文件夾中的所有Java文件
java {
exclude 'debug/**'
}
}
}
}
}
- 將通過(guò)修改
SourceSets
中的屬性,可以指定需要被編譯的源文件,如果是library,則編譯 manifest 下AndroidManifest.xml
,反之則直接編譯 debug 目錄下AndroidManifest.xml
,同時(shí)加入Application
和intent-filter
等參數(shù)。
存疑一:
至于模塊單獨(dú)編譯單獨(dú)運(yùn)行,這種是一個(gè)偽需求,實(shí)際上必然存在多個(gè)模塊間通信的場(chǎng)景。不然跨模塊的服務(wù)提取和獲取,初始化任務(wù),模塊間的聯(lián)合測(cè)試該怎么解決呢?一個(gè)模塊運(yùn)行后需要和其他的模塊通信,比如對(duì)外提供服務(wù),獲取服務(wù),與之相關(guān)聯(lián)的模塊如果沒(méi)有運(yùn)行起來(lái)的話是無(wú)法使用的。
與此同時(shí)還需要在 suorceSets 下維護(hù)兩套 AndoidManifest
以及 Javasource 目錄,這個(gè)不僅麻煩而且每次更改都需要同步一段時(shí)間。所以這種流傳的模塊化獨(dú)立編譯的形式,是否真的適合就仁者見(jiàn)仁了。
三、寫在最后
如需要更詳細(xì)的代碼可以到項(xiàng)目源碼中查看,地址在下面給出。由于時(shí)間倉(cāng)促,項(xiàng)目中有部分功能尚未完善,或者部分實(shí)現(xiàn)方式有待優(yōu)化,也有更多的Jetpack組件尚未在項(xiàng)目中實(shí)踐,比如 依賴注入Hilt
,相機(jī)功能CameraX
,權(quán)限處理Permissions
, 分頁(yè)處理Paging
等等。項(xiàng)目的持續(xù)迭代更新依然是一項(xiàng)艱苦持久戰(zhàn)。
除去可以學(xué)到 Kotlin + MVVM + Android Jetpack + 協(xié)程 + Flow + 組件化 + 模塊化 + 短視頻
的知識(shí),相信你還可以在我的項(xiàng)目中學(xué)到:
- 如何使用 Charles 抓包。
- 提供大量擴(kuò)展函數(shù),快速開(kāi)發(fā),提高效率。
-
ChipGroup
和FlexboxLayoutManager
等多種原生方式實(shí)現(xiàn)流式布局。 - 符合阿里巴巴 Java 開(kāi)發(fā)規(guī)范和阿里巴巴 Android 開(kāi)發(fā)規(guī)范,并有良好的注釋。
-
CoordinatorLayout
和Toolbar
實(shí)現(xiàn)首頁(yè)欄目吸頂效果和輪播圖電影效果。 - 利用
ViewOutlineProvider
給控件添加圓角,大大減少手寫 shape 圓角 xml。 -
ConstraintLayout
的使用,幾乎每個(gè)界面布局都采用的ConstraintLayout
。 - 異步任務(wù)啟動(dòng)器,優(yōu)雅地處理 Application 中同步初始化任務(wù)問(wèn)題,有效減少 APP啟動(dòng)耗時(shí)。
- 無(wú)論是模塊化或者組件化,它們本質(zhì)思想都是一樣的,都是化整為零,化繁為簡(jiǎn),兩者的目的都是為了重用和解耦,只是叫法不一樣。
項(xiàng)目地址:ST_Wan_Android
點(diǎn)關(guān)注,不迷路
好了各位,以上就是這篇文章的全部?jī)?nèi)容了,很感謝您閱讀這篇文章。我是suming,感謝支持和認(rèn)可,您的點(diǎn)贊就是我創(chuàng)作的最大動(dòng)力。山水有相逢,我們下篇文章見(jiàn)!
本人水平有限,文章難免會(huì)有錯(cuò)誤,請(qǐng)批評(píng)指正,不勝感激 !
感謝
API: 鴻洋提供的 WanAndroid API文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-697455.html
主要使用的開(kāi)源框架:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-697455.html
- Retrofit
- OkHttp
- Glide
- ARouter
- MMKV
- RxPermission
- SmartRefreshLayout
希望我們能成為朋友,在 Github、博客 上一起分享知識(shí),一起共勉!Keep Moving!
到了這里,關(guān)于大型Android項(xiàng)目架構(gòu):基于組件化+模塊化+Kotlin+協(xié)程+Flow+Retrofit+Jetpack+MVVM架構(gòu)實(shí)現(xiàn)WanAndroid客戶端的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!