Jetpack Compose(3) —— 狀態管理

来源:https://www.cnblogs.com/joy99/p/18069453
-Advertisement-
Play Games

本文主要講解了 Compose 中狀態的概念。最後做個小結, - Compose UI 依賴狀態變化,觸發重組,驅動界面更新。 - 使用 remember 和 rememberSaveable 進行狀態持久化。remember 保證在 recompose 過程中狀態穩定,rememberSaveab... ...


上一篇文章拿 TextField 組件舉例時,提到了 State,即狀態。本篇文章,即講解 State 的相關改概念。

一、什麼是狀態

與其它聲明式 UI 框架一樣,Compose 的職責非常單純,僅作為對數據狀態的反應。如果數據狀態沒有改變,則 UI 永遠不會自行改變。在 Compose 中,每一個組件都是一個被 @Composable 修飾的函數,其狀態就是函數的參數,當參數不變,則函數的輸出就不會變,唯一的參數決定唯一輸出。反言之,如果要讓界面發生變化,則需要改變界面的狀態,然後 Composable 響應這種變化。
下麵還是拿個例子來說,做一個簡單的計數器,有一個顯示計數的控制項,一個增加的按鈕,每點擊一次,則技術計數器加 1 ,一個減少的按鈕,每點擊一次,計時器減 1。
假如我們用此前的 View 視圖體系,來寫這個方法。代碼大概像下麵這樣:

class MainActivity : AppCompatActivity() {
    // ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        binding.incrementBtn.setOnClickListener {
            binding.tvCounter.text = "${Integer.valueOf(binding.tvCounter.text.toString()) + 1 }"
        }

        binding.decrementBtn.setOnClickListener {
            binding.tvCounter.text = "${Integer.valueOf(binding.tvCounter.text.toString()) - 1 }"
        }
    }
}

顯然上面這個代碼,計數邏輯和 UI 的耦合度就很高。稍微優化一下:

class MainActivity : AppCompatActivity() {
    // ...
    private var counter: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        binding.incrementBtn.setOnClickListener {
            counter++
            updateCounter()
        }

        binding.decrementBtn.setOnClickListener {
            counter--
            updateCounter()
        }
    }

    private fun updateCounter() {
        binding.tvCounter.text = "$counter"
    }
}

這個代碼的改動主要在於,新增了 counter 用於計數,本質上屬於一種 “狀態上提”, 原本 TextView 內部的狀態 “mText”, 上提到了 Activity 中,這樣,即使更換了計數器的 UI, 計數邏輯依然可以復用。

但是當前的代碼,仍然有一些問題,比如計數邏輯在 Activity 中,無法到其它頁面進行復用,進一步使用 MVVM 結構進行改造。引入 ViewModel, 將狀態從 Activity 中上提到 ViewModel 中。

class CounterViewModel: ViewModel() {
    private var _counter: MutableStateFlow<Int> = MutableStateFlow(0)
    val counter: StateFlow<Int> get() = _counter

    fun incrementCounter() {
        _counter.value++
    }

    fun decrementCounter() {
        _counter.value--
    }
}

class MainActivity : AppCompatActivity() {
    // ...
    private val viewModel: CounterViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        binding.incrementBtn.setOnClickListener {
            viewModel.incrementCounter()
        }

        binding.decrementBtn.setOnClickListener {
            viewModel.decrementCounter()
        }

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.counter.collect {
                    binding.tvCounter.text = $it
                }
            }
        }
    }
}

有 Jetpack 庫使用經驗的應該非常熟悉上面的代碼,將狀態上提到 ViewModel 中,使用 StateFlow 或者 LiveData 包裝起來,在 Ativity 中監聽狀態的變化,從而自動刷新 UI。

下麵,我們在 Compose 中實現上述計數器:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        var counter = 0
        Text(text = "$counter")
        Button(onClick = { counter++ }) {
            Text(text = "increment")
        }
        Button(onClick = { counter-- }) {
            Text(text = "decrement")
        }
    }
}

我們寫出上面的代碼,運行。

結果發現,無論怎麼點擊,Text 顯示的值總是 0 ,我們的計數邏輯沒有生效。為了說明這個問題,現在增加一點日誌:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        var counter = 0
        Log.d("sharpcj", "counter text --> $counter")
        Text(text = "$counter")
        Button(onClick = {
            Log.d("sharpcj", "increment button click ")
            counter++
        }) {
            Text(text = "increment")
        }
        Button(onClick = {
            Log.d("sharpcj", "decrement button click ")
            counter--
        }) {
            Text(text = "decrement")
        }
    }
}

再次運行,點擊按鈕,看到日誌如下:

2024-03-12 21:39:27.530 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:39:30.859 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 21:39:31.309 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 21:39:31.468 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 21:39:31.762 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 21:39:31.927 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 21:39:32.661 21949-21949 sharpcj                 com.sharpcj.hellocompose             D  decrement button click 

我們重新捋一捋,Compose 的組件實際上就是一個個函數,Compose 刷新 UI 的邏輯是,狀態發生變化,觸發了重組,函數被重新調用,然後由於參數發生了變化,函數輸出改變了,最終渲染出的的畫面才會發生變化。
再看上面的代碼,我們期望是定義 counter 作為了 Text 組件的狀態,點擊 Button,改變 counter, 到這裡都沒有問題,那麼問題處在了哪裡呢?問題主要是 counter 發生了變化,沒有觸發重組,即函數沒有被重新調用,日誌也證明瞭這一點。
回看我們上面傳統 View 視圖的寫法,此前,我們改變了狀態,需要主動調用 updateCounter 方法去刷新 UI, 後面經過改造,我們把狀態提升到 ViewModel 中,不論是使用 StateFlow 還是使用 LiveData 包裝後,我們都需要在 Activity 中監聽狀態的變化,才能對狀態的變化做出響應。針對上面的例子,我們現在清楚了,計數器不生效原因在於 counter 改變後,Compose 沒有感知到,沒有觸發重組。下麵需要開始學習 Compose 中的狀態了。

二、Compsoe 中的狀態 State

2.1 State

如同傳統試圖中,需要使用 StateFlow 或者 LiveData 將狀態變數包裝成一個可觀察類型的對象。Compose 中也提供了可觀察的狀態類型,可變狀態類型 MutableState 和 不可變狀態類型 State。我們需要使用 State/MutableState 將狀態變數包裝起來,這樣即可觸發重組。更為方便的是,聲明式 UI 框架中,不需要我們顯示註冊監聽狀態變化,框架自動實現了這一訂閱關係。我們來改寫上面的代碼:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        val counter: MutableState<Int> = mutableStateOf(0)
        Log.d("sharpcj", "counter text --> ${counter.value}")
        Text(text = "${counter.value}")
        Button(onClick = {
            Log.d("sharpcj", "increment button click ")
            counter.value++
        }) {
            Text(text = "increment")
        }
        Button(onClick = {
            Log.d("sharpcj", "decrement button click ")
            counter.value--
        }) {
            Text(text = "decrement")
        }
    }
}

我們使用了 mutableStateOf() 方法初始化了一個 MutableState 類型的狀態變數,並傳入預設值 0 ,使用的時候,需要調用 counter.value
再次運行,結果發現,點擊按鈕,計數器值還是沒有變化,日誌如下:

2024-03-12 21:57:24.773  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:57:31.428  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 21:57:31.437  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:57:31.825  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 21:57:31.834  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:57:33.047  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 21:57:33.055  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:57:33.216  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 21:57:33.224  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:57:33.634  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 21:57:33.643  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 21:57:33.792  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 21:57:33.801  6791-6791  sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0

和上一次不一樣了,這次發現,點擊按鈕之後, Text(text = "${counter.value}") 有重新執行,即發生了重組,但是執行的時候,參數沒有改變,依然是 0,其實這裡涉及到一個重組作用域的概念,就是重組是有一個範圍的,關於重組作用範圍,稍後再講。這裡需要知道,發生了重組,Text(text = "${counter.value}") 有重新執行,那麼 val counter: MutableState<Int> = mutableStateOf(0) 也有重新執行,相當於重組時,counter 被重新初始化了,並賦予了預設值 0 。所以點擊按鈕發生了重組,但是計數器的值沒有發生改變。要解決這個問題,則需要使用到 Compose 中的一個重要函數 remember

2.2 remember

我們先看看 remember 函數的源碼:

/**
 * Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
 * Recomposition will always return the value produced by composition.
 */
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

remember 方法的作用是,對其包裹起來的變數值進行緩存,後續發生重組過程中,不會重新初始化,而是直接從緩存中取。具體使用如下:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        val counter: MutableState<Int> = remember { mutableStateOf(0) }
        Log.d("sharpcj", "counter text --> ${counter.value}")
        Text(text = "${counter.value}")
        Button(onClick = {
            Log.d("sharpcj", "increment button click ")
            counter.value++
        }) {
            Text(text = "increment")
        }
        Button(onClick = {
            Log.d("sharpcj", "decrement button click ")
            counter.value--
        }) {
            Text(text = "decrement")
        }
    }
}

再次運行,這次終於正常了。

看日誌也正確了。每次點擊都出發了重組,並且 counter 的值也沒有重新初始化。

2024-03-12 22:18:53.744 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 0
2024-03-12 22:19:10.397 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 22:19:10.421 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 1
2024-03-12 22:19:10.967 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 22:19:10.981 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 2
2024-03-12 22:19:11.181 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 22:19:11.195 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 3
2024-03-12 22:19:11.649 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 22:19:11.663 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 4
2024-03-12 22:19:11.806 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 22:19:11.821 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 5
2024-03-12 22:19:12.364 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 22:19:12.377 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 4
2024-03-12 22:19:12.640 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 22:19:12.657 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 3
2024-03-12 22:19:13.204 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  increment button click 
2024-03-12 22:19:13.220 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 4
2024-03-12 22:19:13.747 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  decrement button click 
2024-03-12 22:19:13.761 19790-19790 sharpcj                 com.sharpcj.hellocompose             D  counter text --> 3

上面的代碼中,我們創建 State 的方法如下:

val counter: MutableState<Int> = remember { mutableStateOf(0) }

使用時,通過 counter.value 來使用,這樣的代碼看起來就很繁瑣,我們可以進一步精簡寫法。
首先, Kotlin 支持類型推導,所以可以寫成下麵這樣:

val counter = remember { mutableStateOf(0) }

另外,藉助於 Kotlin 委托語法,Compose 實現了委托方式賦值,使用 by 關鍵字即可,用法如下:

var counter by remember { mutableStateOf(0) }

並導入如下方法:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

在使用時,直接使用 counter++counter--

需要註意的一點是,沒有使用委托方式創建的對象,類型是 MutableState 類型,我們用 val 聲明,使用委托方式創建對象,對象類型是 MutableState 包裝的對象類型,這裡由於賦初始值為 0 ,根據類型推導,counter 就是 Int 型,由於要修改 counter 的值,所以須使用 var 將其聲明為一個可變類型對象。

2.3 rememberSaveable

使用 remember 雖然解決了重組過程中,狀態被重新初始化的問題,但是當 Activity 銷毀重建時,狀態值依然會重新初始化,比如橫豎屏旋轉,UiMode 切換等場景。在傳統試圖體系中,也存在這樣的問題,對此的解決方案有很多,比如重寫 Activity 的回調方法,在合適的時機,對數據進行保存和恢復,又或者使用 ViewModel 存放數據,這些方法對於 Compose 當然也有效,但是考慮到在使用 Compose 時,應該弱化 Activity 生命周期的概念,所以前者不適合在 Compose 中使用,而使用 ViewModel 依然是一種優秀的選擇,後文再介紹。但是把所有的數據都放到 ViewModel 中,是否是最好的呢,這個要根據具體場景,進行甄別。舉個例子,
針對這種場景,Compose 提供了 rememberSaveable 這個方法來解決這種場景的問題。

var counter by rememberSaveable { mutableStateOf(0) }

用法與 remember 方法用法類似,區別在於,rememberSaveable 在橫豎屏旋轉,UiMode 切換等場景中,能夠對其包裹的數據進行緩存。那是否說明 rememberSaveable 可以在所有的場景替換 remember , remember 方法就沒用了? rememberSaveable 方法比 remember 方法功能更強勁,代價就是性能要差一些,具體使用根據實際場景來選擇。

到這裡,狀態相關的知識點,應該就很清楚了,再回頭看上一篇文章中的 TextField 組件,應該能明白為什麼那樣寫了。

三、 Stateless 和 Stateful

聲明式 UI 的組件一般都可以分為 Stateless 組件和 Stateful 組件。
所謂 stateless 是指這個組件除了依賴參數以外,不依賴其它任何狀態。比如 Text 組件,

Text("Hello, Compose")

相對的,某個組件除了參數以外,還持有或者訪問了外部的狀態,稱為 stateful 組件。比如上一篇文章中提到的 TextField 組件,

var text by remember { mutableStateOf("文本框初始值") }
TextField(value = text, onValueChange = {
    text = it
})

Stateless 是不依賴於外部狀態,僅依賴傳入進來的參數,它是一個“純函數”,即唯一輸入,對應唯一輸出。也就是參數不變,UI 就不會變化,它的重組只能是來自上層的調用,因此 Compose 編譯器對其進行了優化,當 Stateless 的參數沒有變化時,它就不會參與重組,重組的範圍局限於 Stateless 外部。另外 Stateless 不耦合任何業務,功能更純粹,所以復用性更好,也更容易測試。
基於此,我們應該儘可能地將 stateful 組件改造成 stateless 組件,這個過程稱之為狀態上提。

3.1 狀態上提

狀態上提,通常的做法就是將內部狀態移除,以參數的形式傳入。以及需要回調給調用方的事件,也以參數形式傳入。
還是以上面計數器的代碼為例,為了簡潔,去掉前面添加的 log, 代碼如下:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        var counter by remember{ mutableStateOf(0) }
        Text(text = "$counter")
        Button(onClick = {
            counter++
        }) {
            Text(text = "increment")
        }
        Button(onClick = {
            counter--
        }) {
            Text(text = "decrement")
        }
    }
}

這裡計數器主要是依賴了內部狀態 counter, 同時兩個按鈕的點擊事件,會改變 counter。狀態上提之後,該方法如下:

@Composable
fun CounterPage(counter: Int, onIncrement: () -> Unit, onDecrement: () -> Unit) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "$counter")
        Button(onClick = {
            onIncrement()
        }) {
            Text(text = "increment")
        }
        Button(onClick = {
            onDecrement()
        }) {
            Text(text = "decrement")
        }
    }
}

這樣,Counter 組件,就變成了 stateless 組件,不再與業務耦合,職責更加單一,可復用性和可測試性都更強了。此外,狀態上提,有助於單一數據源模型的打造。

四、狀態管理

我們再來看一下在 Compose 中應該如何管理狀態。

4.1 使用 stateful 管理狀態

簡單的的 UI 狀態,且與業務無關的狀態,適合在 Compose 中直接管理。
比如我有一個菜單列表,點一開關,展開一個菜單,再點一下,收起菜單,列表的狀態,僅由點擊開關這一單一事件決定。並且,列表的狀態與任何外部業務無關。那麼這種就適合在 Compose 內部進行管理。

4.2 使用 StateHolder 管理狀態

當業務有一定的複雜度之後,我們可以將業務邏輯相關的狀態統一封裝到一個 StateHoler 進行管理。剝離 Ui 邏輯,讓 Composable 專註 UI 佈局。

4.3 使用 ViewModel 管理狀態

從某種意義上講,ViewModel 也是一種特殊的 StateHolde。單因為它是保存在 ViewModelStore 中,所以有一下特點:

  • 存活範圍大,可以脫離 Composition 存在,被所有 Composable 共用。
  • 存活時間長,不會因為橫豎屏切換或者 UiMode 切換導致數據丟失。

因此,ViewModel 適合管理應用程式全局狀態,而且 ViewModel更傾向於管理哪些非 UI 的業務狀態。

以上管理方式可以同時使用,結合具體的業務靈活搭配。

4.4 LiveData、Rxjava、Flow 轉 State

在 MVVM 架構中,使用 ViewModel 來管理狀態,如果是新項目,把狀態直接定義 State 類型就可以了。

對於傳統試圖項目,一般使用 LiveData、Rxjava 或者 Flow 這類響應式數據框架。而在 Compose 中需要 State 觸發重組,刷新 UI,也有相應的方法,將上述響應式數據流轉換為 Compose 中的 State。當上有數據變化時,可以驅動 Composable 完成重組。具體方法如下:

拓展方法 依賴庫
LiveData.observeAsState() androidx.compose:runtime-livedata
Flow.collectAsState() 不依賴三方庫,Compose 自帶
Observable.subscribeAsState() androidx.compose:runtime-rxjava2 或者 androidx.compose:runtime-rxjava3

五、小結

本文主要講解了 Compose 中狀態的概念。最後做個小結,

  • Compose UI 依賴狀態變化,觸發重組,驅動界面更新。
  • 使用 remember 和 rememberSaveable 進行狀態持久化。remember 保證在 recompose 過程中狀態穩定,rememberSaveable 保證 Activity 自動銷毀重建過程中狀態穩定。
  • 狀態上提,儘可能將 Stateful 組件轉換為 Stateless 組件。
  • 視情況使用 Stateful、StateHoler、ViewModel 管理狀態。
  • 將 LiveData、RxJava、Flow 數據流轉換為 State。
作者:SharpCJ     作者博客:http://joy99.cnblogs.com/     本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一:雙擊startup.bat但閃退 我們可以用記事本打開startup.bat 在末尾添加一個pause 這樣它就會新建一個視窗停在錯誤的地方 二:根據報錯信息改正 這一步如果有亂碼可以進入tomcat的conf目錄下logging.properties 找到 java.util.logging. ...
  • Prometheus是一個開源的監控和告警工具包,其常用的組件主要包括以下幾個部分: Prometheus Server 功能:Prometheus Server是Prometheus的核心組件,負責定時從被監控組件(如Kubernetes、Docker、主機等)中拉取(pull)數據,並將其存儲在 ...
  • 本文主要學習FreeRTOS任務管理的相關知識,包括FreeRTOS創建/刪除任務、任務狀態、任務優先順序、延時函數、空閑任務和任務調度方法等知識 ...
  • Windows從零安裝WordPress 在Linux中,可以用Linux運維工具配合docker很便捷的安裝並配置MySQL、nginx、php、WordPress,但是Windows伺服器中,我還沒有發現類似的面板,就嘗試學慣用最原始的方法進行安裝。 教程環境:Windows Server 20 ...
  • 概述:樂觀併發控制是處理數據訪問併發的一種策略,通過在更新前檢查版本號或時間戳,確保數據在事務間保持一致性。在MySQL示例中,通過比對版本號,如果發現其他事務已更新數據,則拒絕當前事務的修改,避免潛在的併發衝突。這種機制提高了數據一致性,典型應用包括樂觀鎖的實現。 數據訪問併發是指多個事務或用戶同 ...
  • RDS for MariaDB的實例狀態概覽功能,幫助客戶快速感知資料庫實例的整體健康度,並迅速定位異常,極大簡化了運維操作。 ...
  • 轉載至我的博客 https://www.infrastack.cn ,公眾號:架構成長指南 在併發一致性控制場景中,我們常常用for update悲觀鎖來進行一致性的保證,但是如果不瞭解它的機制,就進行使用,很容易出現事故,比如for update進行了鎖表導致其他請求只能等待,從而拖垮系統,因此了 ...
  • 本文詳細介紹了Libcomm通信庫及其原理,讓我們更好的理解GaussDB(DWS)集群通信中的具體邏輯,對於GaussDB(DWS)通信運維也具備一定的參考意義。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...