国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu)

這篇具有很好參考價(jià)值的文章主要介紹了一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu)

一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch
學(xué)習(xí)Android架構(gòu)的原則:學(xué)習(xí)原則,不要盲目遵循規(guī)則。

本文旨在通過示例演示實(shí)際應(yīng)用:通過示范Android架構(gòu)來進(jìn)行教學(xué)。最重要的是,這意味著展示出如何做出各種架構(gòu)決策。在某些情況下,我們會(huì)遇到幾個(gè)可能的答案,而在每種情況下,我們都會(huì)依靠原則而不是機(jī)械地記住一套規(guī)則。

因此,讓我們一起構(gòu)建一個(gè)應(yīng)用。

介紹我們要構(gòu)建的應(yīng)用

我們要為行星觀測者構(gòu)建一個(gè)應(yīng)用。它將大致如下所示:

一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch
我們的應(yīng)用將具有以下功能:

  1. 已發(fā)現(xiàn)的所有行星的列表
  2. 添加剛剛發(fā)現(xiàn)的新行星的方式
  3. 刪除行星的方法(以防你意識(shí)到你的發(fā)現(xiàn)實(shí)際上只是望遠(yuǎn)鏡鏡頭上的污跡)
  4. 添加一些示例行星,讓用戶了解應(yīng)用的工作方式
    它將具有離線數(shù)據(jù)緩存以及在線訪問數(shù)據(jù)庫的功能。

像往常一樣,在我的步驟指導(dǎo)中,我鼓勵(lì)你偏離常規(guī):添加額外的功能,考慮可能的未來規(guī)格變化,挑戰(zhàn)自己。在這里,學(xué)習(xí)的重點(diǎn)是代碼背后的思考過程,而不僅僅是代碼本身。因此,如果你想從這個(gè)教程中獲得最佳效果,請(qǐng)不要盲目復(fù)制代碼。

這是我們最終將得到的代碼庫鏈接:

https://github.com/tdcolvin/PlanetSpotters

介紹我們將要使用的架構(gòu)原則

我們將受到SOLID原則、清晰架構(gòu)原則和谷歌的現(xiàn)代應(yīng)用架構(gòu)原則的啟發(fā)。

我們不會(huì)將這些原則視為硬性規(guī)則,因?yàn)槲覀冏銐蚵斆?,可以?gòu)建適合我們應(yīng)用的東西(特別是對(duì)應(yīng)我們預(yù)期的應(yīng)用增長)。例如,如果你對(duì)清晰架構(gòu)如宗教般追隨,你會(huì)產(chǎn)生穩(wěn)固、可靠、可擴(kuò)展的軟件,但你的代碼可能對(duì)于一個(gè)單一用途的應(yīng)用來說會(huì)過于復(fù)雜。谷歌的原則產(chǎn)生了更簡單的代碼,但如果某天該應(yīng)用可能由多個(gè)大型開發(fā)團(tuán)隊(duì)維護(hù),那就不太合適了。

我們將從谷歌的拓?fù)浣Y(jié)構(gòu)開始,途中會(huì)受到清晰架構(gòu)的啟示。

谷歌的拓?fù)浣Y(jié)構(gòu)如下:
一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch
讓我們逐步實(shí)現(xiàn)這一架構(gòu),并且在我最近的一篇文章中,對(duì)每個(gè)部分進(jìn)行更深入的探討。但是作為簡要概述:

UI層(UI Layer)

UI層實(shí)現(xiàn)了用戶界面。它分為:

  • UI元素,這是用于在屏幕上繪制內(nèi)容的所有專有代碼。在Android中,主要選擇是Jetpack Compose(在這種情況下,@Composables放在這里)或XML(在這種情況下,這里包含XML文件和資源)。
  • 狀態(tài)持有者,這是您實(shí)現(xiàn)首選MVVM / MVC / MVP等拓?fù)涞牡胤健T谶@個(gè)應(yīng)用程序中,我們將使用視圖模型。

領(lǐng)域?qū)?Domain Layer)

領(lǐng)域?qū)佑糜诎呒?jí)業(yè)務(wù)邏輯的用例。例如,當(dāng)我們想要添加一個(gè)行星時(shí),AddPlanetUseCase將描述完成此操作所需的步驟。它是一系列的“what”,而不是“how”:例如,我們會(huì)說“保存行星對(duì)象的數(shù)據(jù)”。這是一個(gè)高級(jí)指令。我們不會(huì)說“將其保存到本地緩存”,更不用說“使用Room數(shù)據(jù)庫將其保存到本地緩存”——這些較低級(jí)別的實(shí)現(xiàn)細(xì)節(jié)放在其他地方。

數(shù)據(jù)層(Data Layer)

谷歌敦促我們?cè)趹?yīng)用程序中擁有一個(gè)數(shù)據(jù)的單一真相來源;也就是說,獲得數(shù)據(jù)絕對(duì)“正確”版本的方法。這就是數(shù)據(jù)層將為我們提供的內(nèi)容(除了描述用戶剛剛輸入的內(nèi)容的數(shù)據(jù)結(jié)構(gòu)之外的所有數(shù)據(jù))。它分為:

  • 存儲(chǔ)庫,管理數(shù)據(jù)類型。例如,我們將有一個(gè)行星數(shù)據(jù)的存儲(chǔ)庫,它將為發(fā)現(xiàn)的行星提供CRUD(創(chuàng)建、讀取、更新、刪除)操作。它還將處理數(shù)據(jù)存儲(chǔ)在本地緩存以及遠(yuǎn)程訪問的情況,選擇適當(dāng)?shù)膩碓磥韴?zhí)行不同種類的操作,并管理兩個(gè)來源包含不同副本數(shù)據(jù)的情況。在這里,我們會(huì)談?wù)摫镜鼐彺娴那闆r,但我們?nèi)匀徊粫?huì)談?wù)撐覀儗⑹褂檬裁吹谌郊夹g(shù)來實(shí)現(xiàn)它。
  • 數(shù)據(jù)源,管理數(shù)據(jù)的存儲(chǔ)方式。當(dāng)存儲(chǔ)庫要求“遠(yuǎn)程存儲(chǔ)X”時(shí),它會(huì)請(qǐng)求數(shù)據(jù)源執(zhí)行此操作。數(shù)據(jù)源僅包含驅(qū)動(dòng)專有技術(shù)所需的代碼——可能是Firebase,或者是HTTP API,或其他什么技術(shù)。

良好的架構(gòu)允許延遲決策

在這個(gè)階段,我們知道應(yīng)用程序的功能將是什么,以及它將如何管理其數(shù)據(jù)的一些基本想法。

還有一些我們尚未決定的事情。我們不知道UI將會(huì)是什么樣子,或者我們將用什么技術(shù)來構(gòu)建它(Jetpack Compose,XML等)。我們不知道本地緩存將采取什么形式。我們不知道我們將使用什么專有解決方案來訪問在線數(shù)據(jù)。我們不知道我們是否將支持手機(jī)、平板電腦或其他形態(tài)因素。

問題:我們需要知道上述任何內(nèi)容來制定我們的架構(gòu)嗎?
答案:不需要!

以上都是低級(jí)考慮因素(在清晰架構(gòu)中,它們的代碼將位于最外層)。它們是實(shí)現(xiàn)細(xì)節(jié),而不是邏輯。SOLID的依賴倒置原則告訴我們,不應(yīng)該編寫依賴于它們的代碼。

換句話說,我們應(yīng)該能夠編寫(和測試?。┢溆嗟膽?yīng)用程序代碼,而無需了解上述任何內(nèi)容。當(dāng)我們確切了解上述問題的答案時(shí),我們已經(jīng)編寫的任何內(nèi)容都不需要更改。

這意味著在設(shè)計(jì)師完成設(shè)計(jì)和利益相關(guān)者決定使用的第三方技術(shù)之前,代碼生產(chǎn)階段就可以開始。因此,良好的架構(gòu)允許延遲決策。(并具有靈活性以在不引起大量代碼混亂的情況下撤銷任何此類決策)。

我們項(xiàng)目的架構(gòu)圖

下面是我們將行星觀測者應(yīng)用程序放入谷歌拓?fù)涞牡谝淮螄L試。

數(shù)據(jù)層(Data Layer)
我們將擁有行星數(shù)據(jù)的存儲(chǔ)庫,以及兩個(gè)數(shù)據(jù)源:一個(gè)用于本地緩存,另一個(gè)用于遠(yuǎn)程數(shù)據(jù)。

UI層(UI Layer)
將有兩個(gè)狀態(tài)持有者,一個(gè)用于行星列表頁,另一個(gè)用于添加行星頁。每個(gè)頁面還將有其一組UI元素,使用的技術(shù)暫時(shí)可以保持不確定。

領(lǐng)域?qū)?Domain layer)
我們有兩種完全有效的方式來構(gòu)建我們的領(lǐng)域?qū)樱?/p>

我們只在重復(fù)業(yè)務(wù)邏輯的地方添加用例。在我們的應(yīng)用程序中,唯一重復(fù)的邏輯是添加行星的地方:用戶在添加示例行星列表時(shí)需要它,手動(dòng)輸入自己的行星詳細(xì)信息時(shí)也需要。因此,我們只會(huì)創(chuàng)建一個(gè)用例:AddPlanetUseCase。在其他情況(例如刪除行星)下,狀態(tài)持有者將直接與存儲(chǔ)庫交互。
我們將每個(gè)與存儲(chǔ)庫的交互都添加為用例,以便狀態(tài)持有者和存儲(chǔ)庫之間永遠(yuǎn)不會(huì)直接聯(lián)系。在這種情況下,我們將有用于添加行星、刪除行星和列出行星的用例。
選項(xiàng)#2的好處是它遵循了清晰架構(gòu)的規(guī)則。但個(gè)人認(rèn)為,對(duì)于大多數(shù)應(yīng)用來說,它稍顯繁重,所以我傾向于選擇選項(xiàng)#1。這也是我們?cè)谶@里要做的。

這給我們帶來了以下的架構(gòu)圖:
一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch

從哪里開始編寫代碼

我們應(yīng)該從哪些代碼開始呢?
規(guī)則是:
從高級(jí)代碼開始,逐步向下編寫。

這意味著首先編寫用例,因?yàn)檫@樣做會(huì)告訴我們對(duì)存儲(chǔ)庫層有什么要求。一旦我們知道存儲(chǔ)庫需要什么,我們就可以寫出數(shù)據(jù)源需要滿足的要求,以便進(jìn)行操作。

同樣,由于用例告訴我們用戶可能采取的所有行動(dòng),我們知道所有輸入和輸出都來自UI。從這些信息中,我們將了解UI需要包含什么內(nèi)容,因此可以編寫狀態(tài)持有者(視圖模型)。然后,有了狀態(tài)持有者,我們就知道需要編寫哪些UI元素。

當(dāng)然,我們可以無限期地延遲編寫UI元素和數(shù)據(jù)源(即所有低級(jí)代碼),直到高級(jí)工程師和項(xiàng)目利益相關(guān)者就要使用的技術(shù)達(dá)成一致。

這樣就結(jié)束了理論部分?,F(xiàn)在讓我們開始構(gòu)建應(yīng)用程序。在進(jìn)行決策時(shí),我將引導(dǎo)您。

第1步:創(chuàng)建項(xiàng)目

打開Android Studio并創(chuàng)建一個(gè)“No Activity”的項(xiàng)目:
一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch
一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch
在下一個(gè)屏幕上,將其命名為PlanetSpotters,并將其他所有內(nèi)容保持不變:添加依賴注入
我們將需要一個(gè)依賴注入框架,這有助于應(yīng)用SOLID的依賴反轉(zhuǎn)原則。在這里,我的首選是Hilt,幸運(yùn)的是,這也是Google專門推薦的選擇。

要添加Hilt,請(qǐng)將以下內(nèi)容添加到根Gradle文件中:然后將其添加到app/build.gradle文件中:(請(qǐng)注意,我們?cè)谶@里設(shè)置兼容性為Java 17,這是Kapt需要的,Hilt使用它。您將需要Android Studio Flamingo或更高版本)。

最后,通過添加@HiltAndroidApp注解來重寫Application類。也就是說,在您的應(yīng)用程序的包文件夾(這里是com.tdcolvin.planetspotters)中創(chuàng)建一個(gè)名為PlanetSpottersApplication的文件,并包含以下內(nèi)容:…然后通過將其添加到清單中,告訴操作系統(tǒng)實(shí)例化它:…一旦我們有了主活動(dòng),我們將需要向其添加@AndroidEntryPoint。但目前,這完成了我們的Hilt設(shè)置。

最后,我們將通過將這些行添加到app/build.gradle來添加對(duì)其他有用庫的支持:第一步:列出用戶可以做和看到的所有內(nèi)容
在編寫用例和存儲(chǔ)庫之前,需要進(jìn)行此步驟。回想一下,用例是用戶可以執(zhí)行的單個(gè)任務(wù),以高層次(what而不是how)描述。

因此,讓我們開始寫出這些任務(wù);一份詳盡的用戶可以在應(yīng)用程序中執(zhí)行和查看的所有任務(wù)列表。

其中一些任務(wù)最終將作為用例編碼。(事實(shí)上,在Clean Architecture下,所有這些任務(wù)都必須作為用例編寫)。其他任務(wù)將由UI層直接與存儲(chǔ)庫層交互。

在此需要一份書面規(guī)范。不需要UI設(shè)計(jì),但如果您有UI設(shè)計(jì),這當(dāng)然有助于可視化。

以下是我們的列表:

  1. 獲取已發(fā)現(xiàn)行星的列表,該列表會(huì)自動(dòng)更新
    輸入:無
    輸出:Flow<List<Planet>>
    動(dòng)作:從存儲(chǔ)庫請(qǐng)求當(dāng)前已發(fā)現(xiàn)行星的列表,以在發(fā)生更改時(shí)保持我們的更新。

  2. 獲取單個(gè)已發(fā)現(xiàn)行星的詳細(xì)信息,該信息會(huì)自動(dòng)更新
    輸入:String-我們要獲取的行星的ID
    輸出:Flow<Planet>
    動(dòng)作:從存儲(chǔ)庫請(qǐng)求具有給定ID的行星,并要求在發(fā)生更改時(shí)保持我們的更新。

  3. 添加/編輯新發(fā)現(xiàn)的行星
    輸入:

    • planetId:String?-如果非空,則為要編輯的行星ID。如果為空,則我們正在添加新行星。
    • name:String-行星的名稱
    • distanceLy:Float-行星到地球的距離(光年)
    • discovered:Date-發(fā)現(xiàn)日期
      輸出:無(通過完成沒有異常來確定成功)

    動(dòng)作:根據(jù)輸入創(chuàng)建一個(gè)Planet對(duì)象,并將其傳遞給存儲(chǔ)庫(以添加到其數(shù)據(jù)源)。

  4. 添加一些示例行星
    輸入:無
    輸出:無
    動(dòng)作:要求存儲(chǔ)庫添加三個(gè)示例行星,發(fā)現(xiàn)日期為當(dāng)前時(shí)間:Trenzalore(300光年),Skaro(0.5光年),Gallifrey(40光年)。

  5. 刪除一顆行星
    輸入:String-要?jiǎng)h除的行星的ID
    輸出:無
    動(dòng)作:要求存儲(chǔ)庫刪除具有給定ID的行星。

現(xiàn)在,我們有了這個(gè)列表,我們可以開始編碼用例和存儲(chǔ)庫。

第2步:編寫用例

根據(jù)第一步,我們有一個(gè)用戶可以執(zhí)行的任務(wù)列表。之前,我們決定將其中的任務(wù)“添加行星”作為用例進(jìn)行編碼。(我們決定僅在應(yīng)用程序的不同區(qū)域重復(fù)任務(wù)時(shí)添加用例)。

這給了我們一個(gè)用例

val addPlanetUseCase: AddPlanetUseCase =//Use our instance as if it were a function:
addPlanetUseCase()

下面是AddPlanetUseCase的實(shí)現(xiàn)代碼:

class AddPlanetUseCase @Inject constructor(private val planetsRepository: PlanetsRepository) {
    suspend operator fun invoke(planet: Planet) {
        if (planet.name.isEmpty()) {
            throw Exception("Please specify a planet name")
        }
        if (planet.distanceLy < 0) {
            throw Exception("Please enter a positive distance")
        }
        if (planet.discovered.after(Date())) {
            throw Exception("Please enter a discovery date in the past")
        }
        planetsRepository.addPlanet(planet)
    }
}

在這里,PlanetsRepository是一個(gè)列出存儲(chǔ)庫將具有的方法的接口。稍后會(huì)更多地介紹這一點(diǎn)(特別是為什么我們創(chuàng)建接口而不是類)。但現(xiàn)在讓我們創(chuàng)建它,這樣我們的代碼就可以編譯通過:

interface PlanetsRepository {
    suspend fun addPlanet(planet: Planet)
}

Planet數(shù)據(jù)類型定義如下:

data class Planet(
    val planetId: String?,
    val name: String,
    val distanceLy: Float,
    val discovered: Date
)

addPlanet方法(就像在使用案例中的invoke函數(shù)一樣)被聲明為suspend,因?yàn)槲覀冎浪鼘⑸婕昂笈_(tái)工作。我們以后會(huì)在這個(gè)接口中添加更多的方法,但目前這就足夠了。

順便說一下,你可能會(huì)問為什么我們費(fèi)力地創(chuàng)建了一個(gè)如此簡單的使用案例。答案在于它未來可能會(huì)變得更加復(fù)雜,并且外部代碼可以與該復(fù)雜性隔離開來。

第2.1步:測試使用案例

我們現(xiàn)在已經(jīng)編寫了使用案例,但我們無法運(yùn)行它。首先,它依賴于PlanetsRepository接口,而我們還沒有它的實(shí)現(xiàn)。Hilt不知道如何處理它。

但是我們可以編寫測試代碼,提供一個(gè)偽造的PlanetsRepository實(shí)例,并使用我們的測試框架運(yùn)行它。這就是你現(xiàn)在應(yīng)該做的。

由于這是關(guān)于架構(gòu)的教程,測試的細(xì)節(jié)不在范圍內(nèi),所以這一步留給你作為練習(xí)。但請(qǐng)注意,良好的架構(gòu)設(shè)計(jì)讓我們將組件拆分為易于測試的部分。

第3步:數(shù)據(jù)層,編寫PlanetsRepository

記住,倉庫的工作是整合不同的數(shù)據(jù)源,處理它們之間的差異,并提供CRUD操作。

使用依賴倒置和依賴注入

根據(jù)干凈架構(gòu)和依賴倒置原則(在我上一篇文章中有更多信息),我們希望避免外部代碼依賴于倉庫實(shí)現(xiàn)內(nèi)部代碼。這樣一來,使用案例或視圖模型(例如)就不會(huì)受到倉庫代碼的更改影響。

這解釋了為什么我們之前將PlanetsRepository創(chuàng)建為接口(而不是類)。調(diào)用代碼將只依賴于接口,但它將通過依賴注入接收實(shí)現(xiàn)。所以現(xiàn)在我們將向接口添加更多方法,并創(chuàng)建其實(shí)現(xiàn),我們將稱之為DefaultPlanetsRepository。

(另外:有些開發(fā)團(tuán)隊(duì)遵循調(diào)用實(shí)現(xiàn)為<interface name>Impl的約定,例如PlanetsRepositoryImpl。我認(rèn)為這種約定不利于閱讀:類名應(yīng)該告訴你為什么要實(shí)現(xiàn)一個(gè)接口。所以我避免使用這種約定。但我提及它是因?yàn)樗粡V泛使用。)

使用Kotlin Flows使數(shù)據(jù)可用

如果你還沒有接觸過Kotlin Flows,請(qǐng)停下手頭的工作,立即閱讀相關(guān)資料。它們將改變你的生活。

https://developer.android.com/kotlin/flow

它們提供了一個(gè)數(shù)據(jù)“管道”,隨著新的結(jié)果變得可用而改變。只要調(diào)用方訂閱了管道,他們將在有變化時(shí)收到更新。因此,現(xiàn)在我們的UI可以在數(shù)據(jù)更新時(shí)自動(dòng)更新,幾乎不需要額外的工作。相比起過去,我們必須手動(dòng)向UI發(fā)出數(shù)據(jù)已更改的信號(hào)。

雖然存在其他類似的解決方案,比如RxJavaMutableLiveData,它們做類似的事情,但它們不如Flows靈活和易于使用。

添加常用的WorkResult

WorkResult類是數(shù)據(jù)層常用的返回類型。它允許我們描述一個(gè)特定請(qǐng)求是否成功,其定義如下:

//WorkResult.kt
package com.tdcolvin.planetspotters.data.repository

sealed class WorkResult<out R> {
    data class Success<out T>(val data: T) : WorkResult<T>()
    data class Error(val exception: Exception) : WorkResult<Nothing>()
    object Loading : WorkResult<Nothing>()
}

調(diào)用代碼可以檢查給定的WorkResultSuccess、Error還是Loading對(duì)象(后者表示尚未完成),從而確定請(qǐng)求是否成功。

第4步: 實(shí)現(xiàn)Repository接口

讓我們把上面的內(nèi)容整合起來,為構(gòu)成我們的PlanetsRepository的方法和屬性制定規(guī)范。

它有兩個(gè)用于獲取行星的方法。第一個(gè)方法通過其ID獲取單個(gè)行星:

fun getPlanetFlow(planetId: String): Flow<WorkResult<Planet?>>

第二個(gè)方法獲取表示行星列表的Flow:

fun getPlanetsFlow(): Flow<WorkResult<List<Planet>>>

這些方法都是各自數(shù)據(jù)的單一來源。每次我們都將返回存儲(chǔ)在本地緩存中的數(shù)據(jù),因?yàn)槲覀冃枰幚磉@些方法被頻繁運(yùn)行的情況,而本地?cái)?shù)據(jù)比訪問遠(yuǎn)程數(shù)據(jù)源更快、更便宜。但我們還需要一種方法來刷新本地緩存。這將從遠(yuǎn)程數(shù)據(jù)源更新本地?cái)?shù)據(jù)源:

suspend fun refreshPlanets()

接下來,我們需要添加、更新和刪除行星的方法:

suspend fun addPlanet(planet: Planet)

suspend fun deletePlanet(planetId: String)

因此,我們的接口現(xiàn)在看起來是這樣的:

interface PlanetsRepository {
    fun getPlanetFlow(planetId: String): Flow<WorkResult<Planet?>>
    fun getPlanetsFlow(): Flow<WorkResult<List<Planet>>>
    suspend fun refreshPlanets()
    suspend fun addPlanet(planet: Planet)
    suspend fun deletePlanet(planetId: String)
}

邊寫代碼邊編寫數(shù)據(jù)源接口

為了編寫實(shí)現(xiàn)該接口的類,我們需要注意數(shù)據(jù)源將需要哪些方法?;叵胍幌?,我們有兩個(gè)數(shù)據(jù)源:LocalDataSourceRemoteDataSource。我們還沒有決定使用哪種第三方技術(shù)來實(shí)現(xiàn)它們——我們現(xiàn)在也不需要。

讓我們現(xiàn)在創(chuàng)建接口定義,準(zhǔn)備好在需要時(shí)添加方法簽名:

//LocalDataSource.kt
package com.tdcolvin.planetspotters.data.source.local

interface LocalDataSource {
  //Ready to add method signatures here...
}
//RemoteDataSource.kt
package com.tdcolvin.planetspotters.data.source.remote

interface RemoteDataSource {
  //Ready to add method signatures here...
}

現(xiàn)在準(zhǔn)備填充這些接口,我們可以編寫DefaultPlanetsRepository了。讓我們逐個(gè)方法來看:

編寫getPlanetFlow()getPlanetsFlow()
這兩個(gè)方法都很簡單;我們返回本地源中的數(shù)據(jù)。(為什么不是遠(yuǎn)程源?因?yàn)楸镜卦吹拇嬖谑菫榱丝焖?、資源輕量級(jí)地訪問數(shù)據(jù)。遠(yuǎn)程源可能始終是最新的,但它較慢。如果我們嚴(yán)格需要最新的數(shù)據(jù),那么在調(diào)用getPlanetsFlow()之前,我們可以使用下面的refreshPlanets()。)

//DefaultPlanetsRepository.kt 
override fun getPlanetsFlow(): Flow<WorkResult<List<Planet>>> {
    return localDataSource.getPlanetsFlow()
}

override fun getPlanetFlow(planetId: String): Flow<WorkResult<Planet?>> {
    return localDataSource.getPlanetFlow(planetId)
}

因此,這取決于LocalDataSource中的getPlanetFlow()getPlanetsFlow()函數(shù)。我們現(xiàn)在將它們添加到接口中,以便我們的代碼可以編譯。

//LocalDataSource.kt
interface LocalDataSource {
    fun getPlanetsFlow(): Flow<WorkResult<List<Planet>>>
    fun getPlanetFlow(planetId: String): Flow<WorkResult<Planet?>>
}

編寫refreshPlanets()方法

為了更新本地緩存,我們從遠(yuǎn)程數(shù)據(jù)源獲取當(dāng)前的行星列表,并將其保存到本地?cái)?shù)據(jù)源中。(然后,本地?cái)?shù)據(jù)源可以“感知”到更改,并通過getPlanetsFlow()返回的Flow發(fā)出新的行星列表。)

//DefaultPlanetsRepository.kt 
override suspend fun refreshPlanets() {
    val planets = remoteDataSource.getPlanets()
    localDataSource.setPlanets(planets)
}

這需要在每個(gè)數(shù)據(jù)源接口中添加一個(gè)新的方法,現(xiàn)在這些接口如下所示:

//LocalDataSource.kt 
interface LocalDataSource {
    fun getPlanetsFlow(): Flow<WorkResult<List<Planet>>>
    fun getPlanetFlow(planetId: String): Flow<WorkResult<Planet?>>
    suspend fun setPlanets(planets: List<Planet>)
}
//RemoteDataSource.kt
interface RemoteDataSource {
    suspend fun getPlanets(): List<Planet>
}

編寫addPlanet()deletePlanet()函數(shù)時(shí),它們都遵循相同的模式:在遠(yuǎn)程數(shù)據(jù)源上執(zhí)行寫操作,如果成功,就將更改反映到本地緩存中。

我們預(yù)計(jì)遠(yuǎn)程數(shù)據(jù)源會(huì)為Planet對(duì)象分配唯一的ID,一旦它進(jìn)入數(shù)據(jù)庫,RemoteDataSource的addPlanet()函數(shù)將返回帶有非空ID的更新后的Planet對(duì)象。

//PlanetsRepository.kt 
override suspend fun addPlanet(planet: Planet) {
    val planetWithId = remoteDataSource.addPlanet(planet)
    localDataSource.addPlanet(planetWithId)
}

override suspend fun deletePlanet(planetId: String) {
    remoteDataSource.deletePlanet(planetId)
    localDataSource.deletePlanet(planetId)
}

我們最終的數(shù)據(jù)源接口如下:

//LocalDataSource.kt
interface LocalDataSource {
    fun getPlanetsFlow(): Flow<WorkResult<List<Planet>>>
    fun getPlanetFlow(planetId: String): Flow<WorkResult<Planet?>>
    suspend fun setPlanets(planets: List<Planet>)
    suspend fun addPlanet(planet: Planet)
    suspend fun deletePlanet(planetId: String)
}
//RemoteDataSource.kt 
interface RemoteDataSource {
    suspend fun getPlanets(): List<Planet>
    suspend fun addPlanet(planet: Planet): Planet
    suspend fun deletePlanet(planetId: String)
}

第5步:狀態(tài)持有者,編寫PlanetsListViewModel

回想一下,UI層由UI元素和狀態(tài)持有者層組成:
一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu),Android架構(gòu),android,架構(gòu),android arch

此時(shí)我們?nèi)匀徊恢牢覀儗⑹褂檬裁醇夹g(shù)來繪制UI,所以我們還不能編寫UI元素層。但這沒有問題;我們可以繼續(xù)編寫狀態(tài)持有者,確信一旦我們做出決定,它們就不必改變。這就是優(yōu)秀架構(gòu)的更多好處!

編寫PlanetsListViewModel的規(guī)范
UI將有兩個(gè)頁面,一個(gè)用于列出和刪除行星,另一個(gè)用于添加或編輯行星。 PlanetsListViewModel負(fù)責(zé)前者。這意味著它需要向行星列表屏幕的UI元素公開數(shù)據(jù),并且必須準(zhǔn)備好接收來自UI元素的事件,以便用戶執(zhí)行操作。

具體而言,我們的PlanetsListViewModel需要公開:

  • 描述頁面當(dāng)前狀態(tài)的Flow(關(guān)鍵是包括行星列表)
  • 刷新列表的方法
  • 刪除行星的方法
  • 添加一些示例行星的方法,以幫助用戶了解應(yīng)用程序的功能

PlanetsListUiState對(duì)象:頁面的當(dāng)前狀態(tài)

我發(fā)現(xiàn)將頁面的整個(gè)狀態(tài)封裝在一個(gè)單獨(dú)的數(shù)據(jù)類中非常有用:

//PlanetsListViewModel.kt
data class PlanetsListUiState(
    val planets: List<Planet> = emptyList(),
    val isLoading: Boolean = false,
    val isError: Boolean = false
)

注意我已經(jīng)在與視圖模型相同的文件中定義了這個(gè)類。它僅包含簡單的對(duì)象:沒有Flows等,只有原始類型,數(shù)組和簡單的數(shù)據(jù)類。請(qǐng)注意,所有字段都有默認(rèn)值-這在后面會(huì)有幫助。

(有一些很好的原因,你可能甚至不希望在上面的類中出現(xiàn)Planet對(duì)象。Clean Architecture的純粹主義者會(huì)指出,在定義Planet的位置和使用它的位置之間有太多的層級(jí)跳轉(zhuǎn)。狀態(tài)提升原則告訴我們只提供我們需要的確切數(shù)據(jù)。例如,現(xiàn)在我們只需要Planet的名稱和距離,所以我們應(yīng)該只有這些,而不是整個(gè)Planet對(duì)象。個(gè)人認(rèn)為這樣做會(huì)不必要地使代碼復(fù)雜化,并且會(huì)使將來的更改更加困難,但你可以自由選擇不同意見!)

因此,定義了這個(gè)類后,我們現(xiàn)在可以在視圖模型內(nèi)部創(chuàng)建一個(gè)狀態(tài)變量來公開它:

//PlanetsListViewModel.kt
package com.tdcolvin.planetspotters.ui.planetslist

...

@HiltViewModel
class PlanetsListViewModel @Inject constructor(
    planetsRepository: PlanetsRepository
): ViewModel() {
    private val planets = planetsRepository.getPlanetsFlow()

    val uiState = planets.map { planets ->
        when (planets) {
            is WorkResult.Error -> PlanetsListUiState(isError = true)
            is WorkResult.Loading -> PlanetsListUiState(isLoading = true)
            is WorkResult.Success -> PlanetsListUiState(planets = planets.data)
        }
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = PlanetsListUiState(isLoading = true)
    )
}

注意在.stateIn(...)中使用的scopestarted參數(shù)可以安全地限制此StateFlow的生命周期。

添加示例行星

為了添加我們的3個(gè)示例行星,我們重復(fù)調(diào)用了為此目的創(chuàng)建的用例。

//PlanetsListViewModel.kt 
fun addSamplePlanets() {
    viewModelScope.launch {
        val planets = arrayOf(
            Planet(name = "Skaro", distanceLy = 0.5F, discovered = Date()),
            Planet(name = "Trenzalore", distanceLy = 5F, discovered = Date()),
            Planet(name = "Galifrey", distanceLy = 80F, discovered = Date()),
        )
        planets.forEach { addPlanetUseCase(it) }
    }
}

刷新和刪除

刷新和刪除函數(shù)非常類似,只需調(diào)用相應(yīng)的存儲(chǔ)庫函數(shù)即可。

//PlanetsListViewModel.kt
fun deletePlanet(planetId: String) {
    viewModelScope.launch {
        planetsRepository.deletePlanet(planetId)
    }
}

fun refreshPlanetsList() {
    viewModelScope.launch {
        planetsRepository.refreshPlanets()
    }
}

第6步:編寫AddEditPlanetViewModel

AddEditPlanetViewModel用于管理用于添加新行星或編輯現(xiàn)有行星的屏幕。

與之前的做法一樣——實(shí)際上,對(duì)于任何視圖模型來說,這都是一個(gè)很好的實(shí)踐——我們將為UI顯示的所有內(nèi)容定義一個(gè)數(shù)據(jù)類,并為其創(chuàng)建單一的數(shù)據(jù)來源。

//AddEditPlanetViewModel.kt
data class AddEditPlanetUiState(
    val planetName: String = "",
    val planetDistanceLy: Float = 1.0F,
    val planetDiscovered: Date = Date(),
    val isLoading: Boolean = false,
    val isPlanetSaved: Boolean = false
)

@HiltViewModel
class AddEditPlanetViewModel @Inject constructor(): ViewModel() {
    private val _uiState = MutableStateFlow(AddEditPlanetUiState())
    val uiState: StateFlow<AddEditPlanetUiState> = _uiState.asStateFlow()
}

如果我們正在編輯一個(gè)行星(而不是添加新行星),我們希望視圖的初始狀態(tài)反映該行星的當(dāng)前狀態(tài)。

作為良好的實(shí)踐,這個(gè)屏幕只會(huì)傳遞我們要編輯的行星的ID。(我們不傳遞整個(gè)行星對(duì)象——它可能會(huì)變得過于龐大和復(fù)雜)。Android的生命周期組件提供了SavedStateHandle,我們可以從中獲取行星ID并加載行星對(duì)象。

//AddEditPlanetViewModel.kt 
@HiltViewModel
class AddEditPlanetViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val planetsRepository: PlanetsRepository
): ViewModel() {
    private val planetId: String? = savedStateHandle[PlanetsDestinationsArgs.PLANET_ID_ARG]

    private val _uiState = MutableStateFlow(AddEditPlanetUiState())
    val uiState: StateFlow<AddEditPlanetUiState> = _uiState.asStateFlow()

    init {
        if (planetId != null) {
            loadPlanet(planetId)
        }
    }

    private fun loadPlanet(planetId: String) {
        _uiState.update { it.copy(isLoading = true) }
        viewModelScope.launch {
            val result = planetsRepository.getPlanetFlow(planetId).first()
            if (result !is WorkResult.Success || result.data == null) {
                _uiState.update { it.copy(isLoading = false) }
            }
            else {
                val planet = result.data
                _uiState.update {
                    it.copy(
                        isLoading = false,
                        planetName = planet.name,
                        planetDistanceLy = planet.distanceLy,
                        planetDiscovered = planet.discovered
                    )
                }
            }
        }
    }
}

注意我們?nèi)绾问褂靡韵履J礁耈I狀態(tài):

_uiState.update { it.copy( ... ) }

在一行簡單的代碼中,它創(chuàng)建一個(gè)新的AddEditPlanetUiState,其值是從先前的狀態(tài)復(fù)制過來的,并通過uiState Flow發(fā)送出去。

這里是我們用該技術(shù)更新行星的各種屬性的函數(shù):

//AddEditPlanetViewModel.kt
fun setPlanetName(name: String) {
    _uiState.update { it.copy(planetName = name) }
}

fun setPlanetDistanceLy(distanceLy: Float) {
    _uiState.update { it.copy(planetDistanceLy = distanceLy) }
}

最后,我們使用AddPlanetUseCase保存行星對(duì)象:

//AddEditPlanetViewModel.kt 
class AddEditPlanetViewModel @Inject constructor(
    private val addPlanetUseCase: AddPlanetUseCase,
    ...
): ViewModel() {

    ...

    fun savePlanet() {
        viewModelScope.launch {
            addPlanetUseCase(
                Planet(
                    planetId = planetId,
                    name = _uiState.value.planetName,
                    distanceLy = uiState.value.planetDistanceLy,
                    discovered = uiState.value.planetDiscovered
                )
            )
            _uiState.update { it.copy(isPlanetSaved = true) }
        }
    }
    
    ...
    
}

第7步:編寫數(shù)據(jù)源和UI元素

現(xiàn)在我們已經(jīng)建立了整個(gè)架構(gòu),可以編寫最低層的代碼,即UI元素和數(shù)據(jù)源。對(duì)于UI元素,我們可以選擇使用Jetpack Compose來支持手機(jī)和平板電腦。對(duì)于本地?cái)?shù)據(jù)源,我們可以編寫一個(gè)使用Room數(shù)據(jù)庫的緩存,而對(duì)于遠(yuǎn)程數(shù)據(jù)源,我們可以模擬訪問遠(yuǎn)程API。

這些層應(yīng)該盡可能保持薄。例如,UI元素的代碼不應(yīng)包含任何計(jì)算或邏輯,只需純粹地將視圖模型提供的狀態(tài)顯示在屏幕上。邏輯應(yīng)該放在視圖模型中。

對(duì)于數(shù)據(jù)源,只需編寫最少量的代碼來實(shí)現(xiàn)LocalDataSourceRemoteDataSource接口中的函數(shù)。

特定的第三方技術(shù)(如Compose和Room)超出了本教程的范圍,但您可以在代碼存儲(chǔ)庫中看到這些層的示例實(shí)現(xiàn)。

將低級(jí)部分留在最后

請(qǐng)注意,我們能夠?qū)⑦@些應(yīng)用的最低級(jí)部分留到最后。這非常有益,因?yàn)樗试S利益相關(guān)者有足夠的時(shí)間來做出關(guān)于使用哪些第三方技術(shù)以及應(yīng)用應(yīng)該如何展示的決策。即使在我們編寫了這些代碼之后,我們也可以更改這些決策,而不會(huì)影響應(yīng)用的其余部分。

Github地址

完整的代碼存儲(chǔ)庫在:

https://github.com/tdcolvin/PlanetSpotters。文章來源地址http://www.zghlxwxcb.cn/news/detail-605773.html

到了這里,關(guān)于一個(gè)靈活、現(xiàn)代的Android應(yīng)用架構(gòu)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • Android入門教程||Android 架構(gòu)||Android 應(yīng)用程序組件

    Android入門教程||Android 架構(gòu)||Android 應(yīng)用程序組件

    Android 操作系統(tǒng)是一個(gè)軟件組件的棧,在架構(gòu)圖中它大致可以分為五個(gè)部分和四個(gè)主要層。 在所有層的最底下是 Linux - 包括大約115個(gè)補(bǔ)丁的 Linux 3.6。它提供了基本的系統(tǒng)功能,比如進(jìn)程管理,內(nèi)存管理,設(shè)備管理(如攝像頭,鍵盤,顯示器)。同時(shí),內(nèi)核處理所有 Linux 所擅

    2024年02月13日
    瀏覽(29)
  • Unity游戲嵌入Android應(yīng)用(融合為一個(gè)應(yīng)用)

    Unity游戲嵌入Android應(yīng)用(融合為一個(gè)應(yīng)用)

    嵌入項(xiàng)目的AndroidStudio版本和Unity版本 Unity2019 AndroidStudio2021 01 新建一個(gè)新的安卓項(xiàng)目 項(xiàng)目里新建一個(gè)button 實(shí)現(xiàn)button的點(diǎn)擊事件進(jìn)入游戲 unity導(dǎo)出android工程 導(dǎo)出的工程文件夾放入原生的安卓項(xiàng)目 放入如下代碼 放入如下代碼 放入如下代碼 項(xiàng)目里添加UnityGameActivity.java 需要配置

    2023年04月08日
    瀏覽(25)
  • 構(gòu)建你的第一個(gè)Android應(yīng)用

    一、Android的核心組件 Android是一種基于Linux的開源操作系統(tǒng),主要用于移動(dòng)設(shè)備,如智能手機(jī)和平板電腦。Android的設(shè)計(jì)目標(biāo)是為用戶提供一個(gè)統(tǒng)一、靈活和豐富的用戶體驗(yàn),同時(shí)保持開放性和兼容性。 Android的核心組件 包括: 應(yīng)用程序框架:提供了一套用于開發(fā)和運(yùn)行應(yīng)用程

    2024年02月01日
    瀏覽(91)
  • 【React Native】第一個(gè)Android應(yīng)用

    【React Native】第一個(gè)Android應(yīng)用

    Windows -- Android 你可以使用任何編輯器來開發(fā)應(yīng)用(編寫 js 代碼),但你仍然必須安裝 Android Studio 來獲得編譯 Android 應(yīng)用所需的工具和環(huán)境 Node 版本請(qǐng)保持在: NodeJs 16.0 請(qǐng)下載 Java SE Development Kit (JDK): JDK \\\\\\\\ 安裝完 NodeJs 請(qǐng)盡量切換 npm 鏡像源 \\\\\\\\ 或使用科學(xué)上網(wǎng)工具 \\\\

    2024年02月03日
    瀏覽(18)
  • 在一個(gè)真實(shí)的設(shè)備上調(diào)試Android應(yīng)用

    由于模擬器只包含很少的應(yīng)用,可能只有一個(gè)處理某個(gè)動(dòng)作的應(yīng)用。為了更好地測試我們的應(yīng)用,需要在一個(gè)真實(shí)的設(shè)備上運(yùn)行這個(gè)應(yīng)用。 可以按一下步驟在一個(gè)真實(shí)設(shè)備上運(yùn)行你的應(yīng)用。 1、啟動(dòng)設(shè)備上的USB調(diào)試選項(xiàng) 在你的安卓設(shè)備上,打開 開發(fā)者選項(xiàng) 。一般情況下,要

    2024年02月02日
    瀏覽(28)
  • 微前端實(shí)戰(zhàn):打造高效、靈活的前端應(yīng)用架構(gòu)

    微前端實(shí)戰(zhàn):打造高效、靈活的前端應(yīng)用架構(gòu)

    隨著互聯(lián)網(wǎng)行業(yè)的快速發(fā)展,前端應(yīng)用的規(guī)模和復(fù)雜度也在不斷增加。為了應(yīng)對(duì)這種挑戰(zhàn),越來越多的企業(yè)和開發(fā)者開始探索新的前端架構(gòu)模式。微前端作為一種新興的前端架構(gòu)模式,憑借其高度模塊化、獨(dú)立部署、易于擴(kuò)展等特點(diǎn),逐漸成為了業(yè)界的熱門話題。本文將通過

    2024年02月05日
    瀏覽(34)
  • Android中Paint字體的靈活使用

    在Android開發(fā)中,Paint是一個(gè)非常重要的繪圖工具,可以用于在控制臺(tái)應(yīng)用程序或Java GUI應(yīng)用程序中繪制各種形狀和圖案。其中,Paint.setText()方法是用于設(shè)置Paint繪制的文本內(nèi)容的。在Android開發(fā)中,如果你想要設(shè)置文本內(nèi)容,那么你必須了解如何使用Paint繪制文本,否則你的應(yīng)用

    2024年02月02日
    瀏覽(13)
  • 開發(fā)一個(gè)Android應(yīng)用:從零到一的實(shí)踐指南

    在這篇博文中,我們將逐步探討如何從頭開始構(gòu)建一個(gè)Android應(yīng)用。我們將從最基本的環(huán)境搭建開始,然后深入討論組件、布局和其他核心概念。在完成整個(gè)過程后,你將會(huì)掌握一個(gè)簡單但完整的Android應(yīng)用開發(fā)流程。讓我們開始吧! 準(zhǔn)備開發(fā)環(huán)境 創(chuàng)建項(xiàng)目 理解項(xiàng)目結(jié)構(gòu) 設(shè)計(jì)

    2024年02月08日
    瀏覽(57)
  • 【Flutter】使用Android Studio 創(chuàng)建第一個(gè)flutter應(yīng)用。

    【Flutter】使用Android Studio 創(chuàng)建第一個(gè)flutter應(yīng)用。

    首先下載好 flutter sdk和 Android Studio。 FlutterSDK下載 Android Studio官網(wǎng) 我的是 windows。 查看flutter安裝環(huán)境。 如果沒有,自己在環(huán)境變量的path添加下flutter安裝路徑。 在將 Path 變量更新后,打開一個(gè)新的控制臺(tái)窗口,然后執(zhí)行下面的命令。如果它提示有任何的平臺(tái)相關(guān)依賴,那么

    2024年02月10日
    瀏覽(29)
  • 構(gòu)建現(xiàn)代應(yīng)用:Java中的熱門架構(gòu)概覽

    構(gòu)建現(xiàn)代應(yīng)用:Java中的熱門架構(gòu)概覽

    ??歡迎來到Java學(xué)習(xí)路線專欄~構(gòu)建現(xiàn)代應(yīng)用:Java中的熱門架構(gòu)概覽 ☆* o(≧▽≦)o *☆嗨~我是IT·陳寒?? ?博客主頁:IT·陳寒的博客 ??該系列文章專欄:Java學(xué)習(xí)路線 ??其他專欄:Java學(xué)習(xí)路線 Java面試技巧 Java實(shí)戰(zhàn)項(xiàng)目 AIGC人工智能 數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí) ??文章作者技術(shù)和水平有限

    2024年02月10日
    瀏覽(29)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包