主頁 > 移動端開發 > [Kotlin Tutorials 19] Kotlin Flows, SharedFlow and StateFlow in Android

[Kotlin Tutorials 19] Kotlin Flows, SharedFlow and StateFlow in Android

2021-09-01 13:06:26 移動端開發

Kotlin Flows

本文包含的內容:

  • Flow是什么, 基本概念和用法.
  • Flow的不同型別, StateFlow和SharedFlow比較.
  • Flow在Android中的使用
    • 安全收集.
    • 運算子stateIn, shareIn的用法和區別.

本文被收錄在集合中: https://github.com/mengdd/KotlinTutorials

Coroutines Flow Basics

Flow是什么

Flow可以按順序發送多個值, 概念上是一個資料流, 發射的值必須是同一個型別.
Flow使用suspend方法來生產/消費值, 資料流可以做異步計算.

幾個基本知識點:

  • 創建flow: 通過flow builders
  • Flow資料流通過emit()來發射元素.
  • 可以通過各種運算子對flow的資料進行處理. 注意中間的運算子都不會觸發flow的資料發送.
  • Flow默認是cold flow, 即需要通過被觀察才能激活, 最常用的運算子是collect().
  • Flow的CoroutineContext, 不指定的情況下是collect()CoroutineContext, 如果想要更改, 用flowOn
    改之前的.

關于Flow的基本用法, 19年底寫的這篇coroutines flow in Android可以溫故知新.

Flow的運算子

一個Flow運算子的可視化小網站: FlowMarbles.

Flow的不同型別

SharedFlow and StateFlow

應用程式里比較常用的型別是SharedFlow和StateFlow.
Android官方有一篇專門的檔案來介紹二者: StateFlow and SharedFlow
StateFlow繼承于SharedFlow, SharedFlow繼承于Flow.

基本關系如下:

kotlin-flows

  • Flow
    基類. Cold.
    Flow的兩大特性: Context preservation; Exception transparency.

  • SharedFlow
    繼承Flow, 是一種hot flow, 所有collectors共享它的值, 永不終止, 是一種廣播的方式.
    一個shared flow上的活躍collector被叫作subscriber.

在sharedFlow上的collect call永遠不會正常complete, 還有Flow.launchIn.
可以配置replay and buffer overflow strategy.

如果subscriber suspend了, sharedflow會suspend這個stream, buffer這個要發射的元素, 等待subscriber resume.
Because onBufferOverflow is set with BufferOverflow.SUSPEND, the flow will suspend until it can deliver the event to all subscribers.

默認引數:

public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)

total buffer是: replay + extraBufferCapacity.
如果total buffer是0, 那么onBufferOverflow只能是onBufferOverflow = BufferOverflow.SUSPEND.

關于reply和buffer, 這個文章
有詳細的解釋, 并且配有動圖.

  • StateFlow
    繼承SharedFlow, hot flow, 和是否有collector收集無關, 永不complete.

可以通過value屬性訪問當前值.
有conflated特性, 會跳過太快的更新, 永遠回傳最新值.
Strong equality-based conflation: 會通過equals()來判斷值是否發生改變, 如果沒有改變, 則不會通知collector.
因為conflated的特性, StateFlow賦值的時候要注意使用不可變的值.

cold vs hot

cold stream 可以重復收集, 每次收集, 會對每一個收集者單獨開啟一次.
hot stream 永遠發射不同的值, 和是否有人收集無關, 永遠不會終止.

  • sharedIn
    可以把cold flow轉成hot的SharedFlow.
  • stateIn
    可以把cold flow轉成hot的StateFlow.

StateFlow vs SharedFlow

共性:

  • StateFlowSharedFlow永遠都不會停止. 不能指望它們的onCompletionCallback.

不同點:

  • StateFlow可以通過value屬性讀到最新的值, 但SharedFlow卻不行.
  • StateFlow是conflated: 如果新的值和舊的值一樣, 不會傳播.
  • SharedFlow需要合理設定buffer和replay策略.

互相轉換:
SharedFlow用了distinctUntilChanged以后變成StateFlow.

// MutableStateFlow(initialValue) is a shared flow with the following parameters:
val shared = MutableSharedFlow(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)
shared.tryEmit(initialValue) // emit the initial value
val state = shared.distinctUntilChanged() // get StateFlow-like behavior

RxJava的等價替代:

  • PublishSubject -> SharedFlow.
  • BehaviorSubject -> StateFlow.

Use Flow in Android

發送事件(Event或Effects): SharedFlow

因為SharedFlow沒有conflated特性, 所以適合發送事件, 即便值變化得快也是每個都發送.

private val _sharedViewEffects = MutableSharedFlow<SharedViewEffects>() // 1
val sharedViewEffects = _sharedViewEffects.asSharedFlow() // 2

這里用了asSharedFlow來創建一個ReadonlySharedFlow.

SharedFlow發射元素有兩個方法:

  • emit: suspend方法.
  • tryEmit: 非suspend方法.

因為tryEmit是非suspend的, 適用于有buffer的情況.

保存暴露UI狀態: StateFlow

StateFlow是一個state-holder, 可以通過value讀到當前狀態值.
一般會有一個MutableStateFlow型別的Backing property.

StateFlow是hot的, collect并不會觸發producer code.
當有新的consumer時, 新的consumer會接到上次的狀態和后續的狀態.

使用StateFlow時, 發射新元素只需要賦值:

mutableState.value = https://www.cnblogs.com/mengdd/archive/2021/08/30/newState

注意這里新值和舊的值要equals判斷不相等才能發射出去.

StateFlow vs LiveData

StateFlowLiveData很像.

StateFlowLiveData的相同點:

  • 永遠有一個值.
  • 只有一個值.
  • 支持多個觀察者.
  • 在訂閱的瞬間, replay最新的值.

有一點點不同:

  • StateFlow需要一個初始值.
  • LiveData會自動解綁, flow要達到相同效果, collect要在Lifecycle.repeatOnLifecycle里.

Flow的安全收集

關于收集Flow的方法, 主要還是關注一下生命周期的問題, 因為SharedFlow和StateFlow都是hot的.
在這個文章里有詳細的討論: A safer way to collect flows from Android UIs

在UI層收集的時候注意要用repeatOnLifecycle:

class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        //...
        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}

這個文章里有個擴展方法也挺好的:

class FlowObserver<T> (
    lifecycleOwner: LifecycleOwner,
    private val flow: Flow<T>,
    private val collector: suspend (T) -> Unit
) {

    private var job: Job? = null

    init {
        lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver {
                source: LifecycleOwner, event: Lifecycle.Event ->
            when (event) {
                Lifecycle.Event.ON_START -> {
                    job = source.lifecycleScope.launch {
                        flow.collect { collector(it) }
                    }
                }
                Lifecycle.Event.ON_STOP -> {
                    job?.cancel()
                    job = null
                }
                else -> { }
            }
        })
    }
}


inline fun <reified T> Flow<T>.observeOnLifecycle(
    lifecycleOwner: LifecycleOwner,
    noinline collector: suspend (T) -> Unit
) = FlowObserver(lifecycleOwner, this, collector)

inline fun <reified T> Flow<T>.observeInLifecycle(
    lifecycleOwner: LifecycleOwner
) = FlowObserver(lifecycleOwner, this, {})

看了一下官方的repeatOnLifecycle其實大概也是這個意思:

public suspend fun Lifecycle.repeatOnLifecycle(
    state: Lifecycle.State,
    block: suspend CoroutineScope.() -> Unit
) {
    require(state !== Lifecycle.State.INITIALIZED) {
        "repeatOnLifecycle cannot start work with the INITIALIZED lifecycle state."
    }

    if (currentState === Lifecycle.State.DESTROYED) {
        return
    }

    // This scope is required to preserve context before we move to Dispatchers.Main
    coroutineScope {
        withContext(Dispatchers.Main.immediate) {
            // Check the current state of the lifecycle as the previous check is not guaranteed
            // to be done on the main thread.
            if (currentState === Lifecycle.State.DESTROYED) return@withContext

            // Instance of the running repeating coroutine
            var launchedJob: Job? = null

            // Registered observer
            var observer: LifecycleEventObserver? = null
            try {
                // Suspend the coroutine until the lifecycle is destroyed or
                // the coroutine is cancelled
                suspendCancellableCoroutine<Unit> { cont ->
                    // Lifecycle observers that executes `block` when the lifecycle reaches certain state, and
                    // cancels when it falls below that state.
                    val startWorkEvent = Lifecycle.Event.upTo(state)
                    val cancelWorkEvent = Lifecycle.Event.downFrom(state)
                    val mutex = Mutex()
                    observer = LifecycleEventObserver { _, event ->
                        if (event == startWorkEvent) {
                            // Launch the repeating work preserving the calling context
                            launchedJob = [email protected] {
                                // Mutex makes invocations run serially,
                                // coroutineScope ensures all child coroutines finish
                                mutex.withLock {
                                    coroutineScope {
                                        block()
                                    }
                                }
                            }
                            return@LifecycleEventObserver
                        }
                        if (event == cancelWorkEvent) {
                            launchedJob?.cancel()
                            launchedJob = null
                        }
                        if (event == Lifecycle.Event.ON_DESTROY) {
                            cont.resume(Unit)
                        }
                    }
                    [email protected](observer as LifecycleEventObserver)
                }
            } finally {
                launchedJob?.cancel()
                observer?.let {
                    [email protected](it)
                }
            }
        }
    }
}

既然官方已經推出了, 我們就用官方的repeatOnLifecycle方法吧.

shareInstateIn

前面提過這兩個運算子是用來做flow轉換的:

  • sharedIn
    可以把cold flow轉成hot的SharedFlow.
  • stateIn
    可以把cold flow轉成hot的StateFlow.

shareIn可以保證只有一個資料源被創造, 并且被所有collectors收集.
比如:

class LocationRepository(
    private val locationDataSource: LocationDataSource,
    private val externalScope: CoroutineScope
) {
    val locations: Flow<Location> = 
        locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}

WhileSubscribed這個策略是說, 當無人觀測時, 上游的flow就被取消.

實際使用時可以用WhileSubscribed(5000), 讓上游的flow即便在無人觀測的情況下, 也能繼續保持5秒.
這樣可以在某些情況(比如旋轉螢屏)時避免重建上游資源, 適用于上游資源創建起來很expensive的情況.

如果我們的需求是, 永遠保持一個最新的cache值.


class LocationRepository(
    private val locationDataSource: LocationDataSource,
    private val externalScope: CoroutineScope
) {
    val locations: Flow<Location> = 
        locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}

Flow.stateIn將會快取最后一個值, 并且有新的collector時, 將這個最新值傳給它.

shareIn, stateIn使用注意事項

永遠不要在方法里面呼叫shareInstateIn, 因為方法每次被呼叫, 它們都會創建新的流.
這些流沒有被復用, 會存在記憶體里面, 直到scope被取消或者沒有參考時被GC.

推薦的使用方式是在property上用:

class UserRepository(
    private val userLocalDataSource: UserLocalDataSource,
    private val externalScope: CoroutineScope
) {
    // DO NOT USE shareIn or stateIn in a function like this.
    // It creates a new SharedFlow/StateFlow per invocation which is not reused!
    fun getUser(): Flow<User> =
        userLocalDataSource.getUser()
            .shareIn(externalScope, WhileSubscribed())    

    // DO USE shareIn or stateIn in a property
    val user: Flow<User> = 
        userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}

StateFlow使用總結

從ViewModel暴露資料到UI, 用StateFlow的兩種方式:

  1. 暴露一個StateFlow屬性, 用WhileSubscribed加上一個timeout.
class MyViewModel(...) : ViewModel() {
    val result = userId.mapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = https://www.cnblogs.com/mengdd/archive/2021/08/30/Result.Loading
    )
}
  1. repeatOnLifecycle收集.
onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
            myViewModel.myUiState.collect { ... }
        }
    }
}

其他的組合都會保持上游的活躍, 浪費資源:

  • WhileSubscribed暴露屬性, 在lifecycleScope.launch/launchWhenX里收集.
  • 通過Lazily/Eagerly暴露, 用repeatOnLifecycle收集.

References

  • Kotlin flows on Android
  • StateFlow and SharedFlow
  • A safer way to collect flows from Android UIs
  • Things to know about Flow’s shareIn and stateIn operators
  • Shared flows, broadcast channels
  • Kotlin SharedFlow or: How I learned to stop using RxJava and love the Flow
  • Migrating from LiveData to Kotlin’s Flow
  • Substituting Android’s LiveData: StateFlow or SharedFlow?
  • Learning State & Shared Flows with Unit Tests
  • Reactive Streams on Kotlin: SharedFlow and StateFlow
  • Reading Coroutine official guide thoroughly — Part 0
作者: 圣騎士Wind
出處: 博客園: 圣騎士Wind
Github: https://github.com/mengdd
微信公眾號: 圣騎士Wind
微信公眾號: 圣騎士Wind

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/296457.html

標籤:其他

上一篇:百度地圖開發-實作離線地圖功能 05

下一篇:百度地圖開發-與地圖的互動功能 06

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more