主頁 > 移動端開發 > 使用Kotlin+協程+MVVM+Jetpack搭建快速開發框架

使用Kotlin+協程+MVVM+Jetpack搭建快速開發框架

2021-02-03 11:09:20 移動端開發

使用Kotlin+協程+MVVM+Jetpack搭建快速開發框架

  • 文章目錄
    • 簡介
      • 相關概念
    • MVVM的具體實作
    • 協程的使用與封裝
    • Retrofit的使用
    • 網路請求的實作
    • 專案地址
    • 總結

文章目錄

簡介

近日有網友在評論區留言,希望我能寫一篇關于kotlin+mvvm的框架相關的博客,于是,筆者與百忙之中,抽出時間,對之前框架做出了相關的整理與優化,并完成了相關技術點的疑難解決,本著開源的相關精神,在此,將相關成果共享給大家,

相關概念

  • Kotlin是由JetBrains創建的基于JVM的靜態編程語言,包含了很多函式式的編程思想的面向物件的的編程語言,kotlin仿佛是為了解決java中存著的一些天生的缺陷而量身定做一般,精簡的語法,null-safety,相對應java8更進一步的lambda運算式支持,使用block完美的代替了介面回呼,自定義擴展函式的使用等等,使得開發者可以撰寫盡量少的樣板代碼,谷歌爸爸早就在2017年就已經宣布kotlin為安卓的官方開發語言了,身為安卓開發從業人員,學習kotlin很有必要,

  • 協程也叫微執行緒,是一種新的多任務并發的操作手段, 協程是創造出來解決異步問題,執行緒的調度是作業系統層面完成是搶占式的;協程是非搶占式的,是協作運行的,是由應用層完成調度,協程在掛起的時候不會堵塞執行緒,只會將當前運行的狀態存在記憶體中,當協程執行完成后,會恢復之前保存的狀態繼續運行,協程的內部是通過狀態機實作的,
    協程具有以下特性

    • 可控制:協程能做到可被控制的發起子任務(協程的啟動和停止都由代碼控制,不像 java)
    • 輕量級:協程非常小,占用資源比執行緒還少
    • 語法糖:使多任務或多執行緒切換不再使用回呼語法
      本框架中使用協程封裝來實作異步的網路請求,
  • MVVM 往往是通過databinding的方式將view層與viewmodel層進行雙向系結,相關的邏輯處理交予viewmodel層中處理,然后通過介面或者livedata的形式傳入到view層中進行相關展示作業,viewmodel中并不持有view的實體,

  • Jetpack 是一個豐富的組件庫,它的組件庫按類別分為 4 類,分別是架構(Architecture)、界面(UI)、行為(behavior)和基礎(foundation),每個組件都可以單獨使用,也可以配合在一起使用,每個組件都給用戶提供了一個標準,能夠幫助開發者遵循最佳做法,減少樣板代碼并撰寫可在各種 Android 版本和設備中一致運行的代碼,讓開發者能夠集中精力撰寫重要的業務代碼,本框架中主要用到了livedata,lifecycles,以及viewmodel,
    在這里插入圖片描述

MVVM的具體實作

主要包括BaseMvvmActivity,BaseViewModel,BaseMvvmView

  • BaseMvvmActivity
abstract class BaseMvvmActivity<V : ViewDataBinding, VM : BaseViewModel> : SwipeBackActivity(),
    BaseMvpView,
    View.OnClickListener,
    LifecycleObserver {
    lateinit var mBinding: V
    lateinit var mViewModel: VM
    private var providerVMClass: Class<VM>? = null
    private var receiver: MyNetBroadCastReciver? = null
    lateinit var mActivity: BaseMvvmActivity<*, *>
    lateinit var mRootView: ViewGroup


  /**
     * 注入系結
     */
    private fun initViewDataBinding() {
        //DataBindingUtil類需要在project的build中配置 dataBinding {enabled true }, 同步后會自動關聯android.databinding包
        mBinding = DataBindingUtil.setContentView(this, layoutId)
        mBinding.setVariable(initVariableId(), mViewModel)
        mBinding.executePendingBindings()

        //liveData系結activity,fragment生命周期
        mBinding.lifecycleOwner = this
        mRootView = mBinding.root as ViewGroup
        mRootViewParent= layoutInflater.inflate(R.layout.activity_base, null) as LinearLayout?

      mRootView.parent?.let {
            (mRootView.parent as ViewGroup).removeView(mRootView)
        }
        mContentView ?.addView(mRootView)
        setContentView(mRootViewParent)
        if (null != intent) handleIntent(intent)
        initView(mRootViewParent!!)

加粗文本

此處有一個要點,由于筆者的開發習慣,總是習慣于,將headview,包括errorviewwaitdialog等app基礎功能封裝到底層,于是在底層進行封裝時,必須將業務層的xml檔案在底層進行重新注入系結,此時就會出現問題:就是將業務層xml放入到基層的xml中,基層相關功能是失效的,查看相關原始碼得知,當我們呼叫DataBindingUtil.setContentView() 方法時,最中還會呼叫到activity.setContentView(),此時在加入到基層的xml中,基層功能會完全失效,而如果是先加入到基層的xml,然后實行DataBinding的系結,則Databinding的雙向系結完全失效,經過筆者的多方除錯以及查看原始碼,解決了此問題,即先將進行系結,然后加入到底層的xml中,注入相關功能,最后再次呼叫activity.setContentView(),一定要注意相關順序,這樣才能同時實作雙向系結和底層多狀態布局的復用,

  • BaseViewModel
open class BaseViewModel : ViewModel(), LifecycleObserver, BaseMvvmView {

    val vStatus: MutableLiveData<Map<String, Any>> = MutableLiveData()

    override fun showWaitDialog() {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.SHOWWAITDIALOG
        vStatus.value = viewStatus
    }

    override fun showWaitDialog(message: String) {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.SHOWWAITDIALOG
        viewStatus["msg"] = message
        vStatus.value = viewStatus
    }

    
    override fun hideWaitDialog() {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.HIDEWAITDIALOG
        vStatus.value = viewStatus
    }

    override fun showToast(msg: String?) {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.SHOWTOAST
        viewStatus["msg"] = msg ?: "error"
        vStatus.value = viewStatus
    }


    override fun showStatusEmptyView(emptyMessage: String) {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.SHOWSTATUSEMPTYVIEW
        viewStatus["msg"] = emptyMessage
        vStatus.value = viewStatus
    }

    override fun showStatusErrorView(emptyMessage: String?) {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.SHOWSTATUSERRORVIEW
        viewStatus["msg"] = emptyMessage ?: "未知錯誤"
        vStatus.value = viewStatus
    }

    override fun showStatusLoadingView(loadingMessage: String) {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.SHOWSTATUSLOADINGVIEW
        viewStatus["msg"] = loadingMessage
        vStatus.value = viewStatus
    }


    override fun hideStatusView() {
        var viewStatus = HashMap<String, Any>()
        viewStatus["status"] = ViewStatusEnum.HIDESTATUSVIEW
        vStatus.value = viewStatus
    }

  
    fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
        viewModelScope.launch { block() }
    }


    suspend fun <T> launchIO(block: suspend CoroutineScope.() -> T) {
        withContext(Dispatchers.IO) {
            block
        }
    }

    fun launch(tryBlock: suspend CoroutineScope.() -> Unit) {
        launchOnUI {
            tryCatch(tryBlock, {}, {})
        }
    }

    fun launchWithTryCatch(
        tryBlock: suspend CoroutineScope.() -> Unit,
        catchBlock: suspend CoroutineScope.(String?) -> Unit,
        finallyBlock: suspend CoroutineScope.() -> Unit
    ) {
        launchOnUI {
            tryCatch(tryBlock, catchBlock, finallyBlock)
        }
    }

    private suspend fun tryCatch(
        tryBlock: suspend CoroutineScope.() -> Unit,
        catchBlock: suspend CoroutineScope.(String?) -> Unit,
        finallyBlock: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            try {
                tryBlock()
            } catch (e: Throwable) {
                catchBlock(e.message)
            } finally {
                finallyBlock()
            }
        }
    }

    /**
     * 網路請求
     *
     */
    fun <T> launchRequest(
        tryBlock: suspend CoroutineScope.() -> Result<T>?,
        successBlock: suspend CoroutineScope.(T?) -> Unit,
        catchBlock: suspend CoroutineScope.(String?) -> Unit,
        finallyBlock: suspend CoroutineScope.() -> Unit
    ) {
        launchOnUI {
            requestTryCatch(tryBlock, successBlock, catchBlock, finallyBlock)
        }
    }

    suspend fun <T> getResopnse(response: Result<T>?): T? {
        if (response == null || EmptyUtils.isEmpty(response)) return null
        if (response.code == 0) return response.result
        else return null
    }

    private suspend fun <T> requestTryCatch(
        tryBlock: suspend CoroutineScope.() -> Result<T>?,
        successBlock: suspend CoroutineScope.(T?) -> Unit,
        catchBlock: suspend CoroutineScope.(String?) -> Unit,
        finallyBlock: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            try {
                var response = tryBlock()
                callResponse(
                    response,
                    {
                        successBlock(response?.result)
                    },
                    {
                        catchBlock(response?.message)
                    }
                )
            } catch (e: Throwable) {
                var errMsg = ""
                when (e) {
                    is UnknownHostException -> {
                        errMsg = "No network..."
                    }
                    is SocketTimeoutException -> {
                        errMsg = "Request timeout..."
                    }
                    is NumberFormatException -> {
                        errMsg = "Request failed, type conversion exception"
                    }
                    else -> {
                        errMsg = e.message.toString()
                        Log.e("xxxxxxxxxx", Gson().toJson(e))
                    }

                }
                catchBlock(errMsg)
            } finally {
                finallyBlock()
            }
        }
    }

    /**
     * 主要用于處理回傳的response是否請求成功
     */
    suspend fun <T> callResponse(
        response: Result<T>?, successBlock: suspend CoroutineScope.() -> Unit,
        errorBlock: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            when {
                response == null || EmptyUtils.isEmpty(response) -> errorBlock()
                response.code == 0 -> successBlock()
                else -> errorBlock()
            }
        }
    }
}

這里筆者在BaseViewModel中實作了BaseMvpView中的眾多方法,然后將相關的狀態通過livedata進行傳遞,這樣可以直接在viewmodel中去呼叫相關的view中的狀態,這一點完全根據個人的開發習慣而來

  • BaseMvpView BaseMvpView中主要定義了一些view中常用的方法
interface BaseMvvmView {

    fun showWaitDialog()

    fun showWaitDialog(message: String)

    fun showWaitDialog(message: String, cancelable: Boolean)

    fun hideWaitDialog()

    fun showToast(msg: String?)

    fun showStatusEmptyView(emptyMessage: String)

    fun showStatusErrorView(emptyMessage: String?)

    fun showStatusLoadingView(loadingMessage: String)

    fun showStatusLoadingView(loadingMessage: String, isHasMinTime: Boolean)

    fun hideStatusView()
}

協程的使用與封裝

  • 協程的引入
    • 首先我們需要在baseUI中引入相應的庫
      implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1’
      implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1’
      如圖
      在這里插入圖片描述
  • 我們需要在gradle腳本中添加coroutines支持
    kotlin {
    experimental {
    coroutines ‘enable’
    }
    }
    在這里插入圖片描述
  • 協程的引入
    我們的BaseViewModel 直接繼承與ViewModel,并實作了LifecycleObserver 進行生命周期的管理,協程可以直接使用ViewModel的viewModelScope即可
 fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
        viewModelScope.launch { block() }
    }
   fun <T> launchRequest(
        tryBlock: suspend CoroutineScope.() -> Result<T>?,
        successBlock: suspend CoroutineScope.(T?) -> Unit,
        catchBlock: suspend CoroutineScope.(String?) -> Unit,
        finallyBlock: suspend CoroutineScope.() -> Unit
    ) {
        launchOnUI {
            requestTryCatch(tryBlock, successBlock, catchBlock, finallyBlock)
        }
    }

launchRequest方法中有四個函式作為引數,
tryBlock:回傳的是一個由Result包裹的泛型引數,主要用于網路請求的呼叫,
successBlock:主要用于請求成功的呼叫,相當要onSuccess回呼,
catchBlock:主要用于請求失敗的呼叫,相當于onError回呼,
finallyBlock:主要用于請求完成的回呼,相當于onComplete回呼

requestTryCatch方法中對協程進行了tryCatch操作,并對結果進行了相應的處理

private suspend fun <T> requestTryCatch(
        tryBlock: suspend CoroutineScope.() -> Result<T>?,
        successBlock: suspend CoroutineScope.(T?) -> Unit,
        catchBlock: suspend CoroutineScope.(String?) -> Unit,
        finallyBlock: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            try {
                var response = tryBlock()
                callResponse(
                    response,
                    {
                        successBlock(response?.result)
                    },
                    {
                        catchBlock(response?.message)
                    }
                )
            } catch (e: Throwable) {
                var errMsg = ""
                when (e) {
                    is UnknownHostException -> {
                        errMsg = "No network..."
                    }
                    is SocketTimeoutException -> {
                        errMsg = "Request timeout..."
                    }
                    is NumberFormatException -> {
                        errMsg = "Request failed, type conversion exception"
                    }
                    else -> {
                        errMsg = e.message.toString()
                    }

                }
                catchBlock(errMsg)
            } finally {
                finallyBlock()
            }
        }
    }

callResponse,用于對請求的結果進行處理,

 suspend fun <T> callResponse(
        response: Result<T>?, successBlock: suspend CoroutineScope.() -> Unit,
        errorBlock: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            when {
                response == null || EmptyUtils.isEmpty(response) -> errorBlock()
                response.code == 0 -> successBlock()
                else -> errorBlock()
            }
        }
    }

Retrofit的使用

retrofit的使用和封裝相信大家都很了解了,這里就不多講了,直接貼出代碼來吧

// An highlighted block
interface ApiServices {
    /**
     * 用戶登錄
     */
    @GET("login?key=00d91e8e0cca2b76f515926a36db68f5")
    fun requestLoginOut( @Query("phone") phone: String,
                         @Query("passwd") passwd: String): Deferred<Result<LoginBean>>

    @GET("createUser?key=00d91e8e0cca2b76f515926a36db68f5")
    fun requestRegister(
        @Query("phone") phone: String,
        @Query("passwd") passwd: String
    ): Deferred<Result<RegisterBean>>

}

ApiHelper初始化okhttp,和Retrofit,

// An highlighted block

object ApiHelper {
    private var api: ApiServices? = null

    fun api(): ApiServices? {
        if (api == null)
            initApi()
        return api
    }

    /**
     * 初始化api
     */
    fun initApi() {
        // Header
        val headerInter = Interceptor { chain ->
            val builder = chain.request()
                .newBuilder()
            chain.proceed(builder.build())
        }

        val mOkHttpClient = OkHttpClient()
            .newBuilder()
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .connectTimeout(20, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .addInterceptor(headerInter)
            .addInterceptor(LoggingInterceptor())
            .build()
        //網路介面配置
        api = null
        api = Retrofit.Builder()
            .baseUrl("https://www.apiopen.top/")
            .addConverterFactory(ScalarsConverterFactory.create())       //添加字串的轉換器
            .addConverterFactory(GsonConverterFactory.create())          //添加gson的轉換器
            .addCallAdapterFactory(CoroutineCallAdapterFactory.invoke())   //添加攜程的請求配接器            .client(mOkHttpClient)
            .client(mOkHttpClient)
            .build()
            .create(ApiServices::class.java)
    }


}

希望大家注意一下這里的Deferred
在這里插入圖片描述
這里的Deferred用于接收一個Coroutine的回傳結果,
協程的請求配接器,需要引入“com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2

網路請求的實作

現在我們可以來使用我們封裝的協程來實作網路請求了,
在presenter層中定義了兩個請求,案例如下

class MainViewModel : BaseViewModel() {
    var edit: MutableLiveData<String> = MutableLiveData()
    var test: MutableLiveData<String> = MutableLiveData()
    fun requestTestData() {
        showWaitDialog()
        launchRequest({
            ApiHelper.api().requestTestApi("utf-8", "衛衣").await()
        }, { data: List<List<String>>? ->
            test.value = data.toString()
        }, { errMsg: String? ->
            showToast(errMsg)
        }, {
            hideWaitDialog()
        })
    }
}

xml檔案中的系結

  <!--單向系結@{}-->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{mainVM.edit}"
            android:layout_marginTop="50dp"
            android:textColor="@color/black"
            android:textSize="25dp" />
        <!--雙向系結@={}-->
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={mainVM.edit}"
            android:layout_marginTop="50dp"
            android:textSize="25dp" />

專案地址

傳送門

此專案已經托管到github并且開源,如果想看原始碼點擊傳送門查看,如果覺得還可以,不妨給在下一個star,謝謝大家,

總結

本片文章主要是為了用協程代替RxJava實作網路請求的異步,使用livedata進行通訊,lifecycles進行生命周期管理,打造Kotlin+協程+mvvm+jetpack的便捷開發架構,

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

標籤:其他

上一篇:react專案運行出現的快取問題導致的編譯失敗

下一篇:Android自定義實作按周簽到打卡功能

標籤雲
其他(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