詳解安卓架構入門

laomuji666發表於2024-04-30

準備

首先進入安卓架構入門的程式碼倉庫:

Android Architecture Starter Templates:
https://github.com/android/architecture-templates

先看看介紹,簡單分析一下:

  • 架構入門的模板
  • UI 介面非常簡陋
  • Navigation 導航
  • 協程和Flow
  • Hilt 依賴注入
  • Hilt 虛假資料進行UI測試

提供了兩個模板,單模組多模組,單模組和多模組沒有絕對的誰好誰壞。

  • 單模組使用起來簡單快速,但開發維護會隨著專案變大越來越難。
  • 多模組會增加額外的負擔,如:不同模組配置Build難以保持一致、模組間的互動需要精細化的設計,但開發維護的難度變化不會太大。

單模組

移除無用程式碼和檔案

先粗略點開app模組的所有程式碼,簡單看看。

build.gradle.kts

plugins上方出現一個 @Suppress (忽略警告的註解)。點開連結,發現問題在Gradle 8.1+版本已經解決,這個註解可以刪掉。

@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369

android內部出現一個 packagingOptions 棄用警告,點開發現被packaging替代,二者的引數是一樣的,都是Packaging介面的無參擴充套件函式,所以可以直接替換。

Theme.kt

發現在 SideEffect(每次重組後都會執行) 內的修改頂部狀態列的程式碼標記棄用。

(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()  
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme

改為:

val window = (view.context as Activity).window  
window.statusBarColor = colorScheme.primary.toArgb()  
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme

MyApplication.kt

就只有一個簡單的 @HiltAndroidApp 註解,Hilt 依賴注入會附加到這個Application的生命週期,並提供依賴項。所有使用Hilt的應用都必須有這個註解。

MainActivity.kt

MainActivity上方有個 @AndroidEntryPoint 註解,也是 Hilt 依賴注入的註解。
setContent 使用了 theme 裡的 MyApplicationTheme ,然後一個鋪滿全屏的背景色。content呼叫了MainNavigation。

使用了NavHostController和NavHost,但和沒有用一樣,就只有一個MyModelScreen。

MyModelScreen.kt

fun MyModelScreen(modifier: Modifier = Modifier, viewModel: MyModelViewModel = hiltViewModel())

引數1 使用了官方推薦的寫法,modifier可以被傳入。
引數2 把viewModel的預設引數設定為hiltViewModel(),交由Hilt注入。也可以由Activity建立ViewModel然後傳遞過來。

val items by viewModel.uiState.collectAsStateWithLifecycle()

uiState 的型別是StateFlow<MyModelUiState>,也就是隻讀型別的Flow,StateFlow和Compose的State無關!
collectAsStateWithLifecycle 是compose為協程增加的生命週期擴充套件的函式之一,可以只在Compose的生命週期裡收集協程傳來的資料。需要新增以下依賴:

androidx.lifecycle:lifecycle-runtime-compose


if (items is MyModelUiState.Success) {  
    MyModelScreen(  
        items = (items as MyModelUiState.Success).data,  
        onSave = viewModel::addMyModel,  
        modifier = modifier  
    )  
}

當item是MyModelUiState.Success型別時,顯示MyModelScreen螢幕。
但是我感覺這裡不應該這樣寫,MyModelUiState密封介面有三個狀態,Loading、Error、Success,應該三種情況都要寫出來,應該改成這樣:

when(items){  
    MyModelUiState.Loading -> {  
        //TODO Loading  
    }  
    is MyModelUiState.Error -> {  
        //TODO ERROR  
    }  
    is MyModelUiState.Success -> {  
        MyModelScreen(  
            items = (items as MyModelUiState.Success).data,  
            onSave = viewModel::addMyModel,  
            modifier = modifier  
        )  
    }  
}

MyModelScreen、DefaultPreview、PortraitPreview 這三個函式就是簡單的繪製和預覽,沒有什麼好說的。

MyModelViewModel.kt

在MyModelViewModel上方出現 @HiltViewModel 註解,使這個ViewModel可以提供給Hilt注入。主建構函式出現 @Inject 註解,注入MyModelRepository到myModelRepository。

sealed interface MyModelUiState {  
    object Loading : MyModelUiState  
    data class Error(val throwable: Throwable) : MyModelUiState  
    data class Success(val data: List<String>) : MyModelUiState  
}

介面狀態,分為三種情況:

  • Loading 載入中
  • Error 載入失敗
  • Success 載入成功

fun addMyModel(name: String) {  
    viewModelScope.launch {  
        myModelRepository.add(name)  
    }  
}

使用和viewModel生命週期繫結的協程,向myModelRepository新增一個name。

val uiState: StateFlow<MyModelUiState> = myModelRepository  
    .myModels.map<List<String>, MyModelUiState>(::Success)  
    .catch { emit(Error(it)) }  
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading)

我把程式碼拆一下:

val uiState: StateFlow<MyModelUiState> = myModelRepository  
    .myModels.map<List<String>, MyModelUiState>{  
        MyModelUiState.Success(it)  
    }  
    .catch {  
        emit(MyModelUiState.Error(it))  
    }  
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MyModelUiState.Loading)
  • myModelRepository .myModels 的型別是 Flow<List<String>>。
  • map是flow的擴充函式,把Flow儲存的A型別轉為B型別。把 List<String> 型別作為入參的型別,把MyModelUiState作為出參的型別。MyModelUiState.Success(it) 裡的 it 就是原本的 List<String> 型別的引數。這裡泛型指定 MyModelUiState 的原因是下方的需要catch需要emit MyModelUiState.Error,如果不需要異常狀態可以去掉這裡的顯式指定異常。
  • stateIn,繫結viewModel的協程生命週期,訂閱者和共享協程停止時的延遲,Flow在collect時預設引數。
  • SharingStarted.WhileSubscribed(5000),在沒有Flow的訂閱者時,會停止collect,延遲設定為5000毫秒,如果5秒內有了新的訂閱者,就不會停止collect。與之對應的是SharingStarted.Lazily,永遠不會停止collect。

MyModelRepository.kt

interface MyModelRepository {  
    val myModels: Flow<List<String>>  
  
    suspend fun add(name: String)  
}
  • myModels 的型別是Flow<List<String>>,用來在協程中收集資料。
  • add 函式前有 suspend 表示需要在協程中執行。

class DefaultMyModelRepository @Inject constructor(  
    private val myModelDao: MyModelDao  
) : MyModelRepository {  
  
    override val myModels: Flow<List<String>> =  
        myModelDao.getMyModels().map { items -> items.map { it.name } }  
  
    override suspend fun add(name: String) {  
        myModelDao.insertMyModel(MyModel(name = name))  
    }  
}
  • myModels透過 myModelDao 獲取 Flow<List<MyModel>>型別的物件轉換為Flow<List<String>>型別。
  • add 透過 myModelDao 插入一個 MyMode l物件。

DataModule.kt

提供虛假的資料給 androidTest 使用,沒什麼特殊的。

DatabaseModule.kt

@Module  
@InstallIn(SingletonComponent::class)

Hilt 單例繫結,注入Application。整個Application只會出現一個例項。

@Provides  
fun provideMyModelDao(appDatabase: AppDatabase): MyModelDao {  
    return appDatabase.myModelDao()  
}

@Provides 作用域是整個生命週期,告訴Hilt,這個函式可以提供MyModelDao型別的物件。
這整段程式碼的意思是,在Hilt註解需要注入MyModelDao型別的物件時,透過這個函式獲取。

@Provides  
@Singleton
fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase {  
    return Room.databaseBuilder(  
        appContext,  
        AppDatabase::class.java,  
        "MyModel"  
    ).build()  
}

@Singleton 整個Application的生命週期只會生成一次。
這整段程式碼的意思是建立一個AppDatabase型別的物件,這段程式碼只會執行一次,後續需要AppDatabase型別的物件時會一直使用這個物件。

AppDatabase.kt

@Database(entities = [MyModel::class], version = 1)  
abstract class AppDatabase : RoomDatabase() {  
    abstract fun myModelDao(): MyModelDao  
}
  • @Database 註解 和 繼承 RoomDatabase,建立一個資料庫。
  • entities = [MyModel::class] 資料庫中有個MyModel型別的表。
  • myModelDao() 獲取MyModelDao例項物件。

MyModel.kt

@Entity  
data class MyModel(  
    val name: String  
) {  
    @PrimaryKey(autoGenerate = true)  
    var uid: Int = 0  
}
  • @Entity 提供給AppDatabase的資料表。
  • name: String 資料表的欄位。
  • @PrimaryKey(autoGenerate = true) 宣告這個資料表的主鍵。

多模組

單模組的程式碼已經講的很詳細了,這裡僅僅講一下差異。

app模組

程式碼檔案:

  • MyApplication.kt
  • MainActivity.kt
  • MainNavigation.kt

build.gradle.kts

implementation(project(":core-ui"))
implementation(project(":feature-mymodel"))


core-ui

之前的theme目錄,放一些通用的compose可組合項,必須是通用的。

core-testing

自定義測試Application。

test-app

之前的 androidTest 目錄。

core-data

程式碼檔案:

  • DataModule.kt
  • MyModelRepository.kt
    之前的data目錄。

core-database

之前的database目錄。

feature-mymodel

之前的mymodel目錄,值得注意的是這裡也有androidTest,這裡的測試針對的是當前模組的,並沒有使用FakeMyModelRepository,而是直接給了個list。可見多模組的情況下,官方對於測試也沒有很優雅的解決方案。

相關文章