在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

移植Mediapipe LLM Demo到Kotlin Multiplatform

谷歌開發者 ? 來源:Android高效開發 ? 2024-12-05 16:29 ? 次閱讀

以下文章來源于Android高效開發,作者2BAB

作者 / Android 谷歌開發者專家 El Zhang (2BAB)

在今年的廈門和廣州 Google I/O Extended 上,我分享了《On-Device Model 集成 (KMP) 與用例》。本文是當時 Demo 的深入細節分析,同時也是后面幾篇同類型文章的開頭。通過本文你將了解到:

移植 Mediapipe 的 LLM Inference Android 官方 Demo 到 KMP,支持在 iOS 上運行。

KMP 兩種常見的調用 iOS SDK 的方式:

Kotlin 直接調用 Cocoapods 引入的第三方庫。

Kotlin 通過 iOS 工程調用第三方庫。

KMP 與多平臺依賴注入時的小技巧 (基于 Koin)。

On-Device Model 與 LLM 模型 Gemma 1.1 2B 的簡單背景。

On-Device Model 本地模型

大語言模型 (LLM) 持續火熱了很長一段時間,而今年開始這股風正式吹到了移動端,包括 Google 在內的最新手機與系統均深度集成了此類 On-Device Model 的相關功能。對于 Google 目前的公開戰略中,On-Device Model 這塊的大語言模型主要分為兩個:

Gemini Nano: 非開源,支持機型較少 (某些機型支持特定芯片加速如 Tensor G4),具有強勁的表現。目前可以在桌面平臺 (Chrome) 和部分 Android 手機上使用 (Pixel 8/9 Samsung 和小米部分機型)。據報道晚些時候會公開給更多的開發者進行使用和測試。

Gemma: 開源,支持所有滿足最低要求的機型,同樣有不俗的性能表現,與 Nano 使用類似的技術路線進行訓練。目前可以在多平臺上體驗 (Android/iOS/Desktop)。

目前多數移動端開發者尚無法直接基于 Gemini Nano 開發,所以今天的主角便是 Gemma 1 的 2B 版本。想在移動平臺上直接使用 Gemma,Google 已給我們提供一個開箱即用的工具: Mediapipe。MediaPipe 是一個跨平臺的框架,它封裝了一系列預構建的 On-Device 機器學習模型和工具,支持實時的手勢識別、面部檢測、姿態估計等任務,還可應用于生成圖片、聊天機器人等各種應用場景。感興趣的朋友可以試玩它的 Web 版 Demo,以及相關文檔。

82cea7ea-b223-11ef-93f3-92fbcf53809c.jpg

而其中的 LLM Inference API (上表第一行),用于運行大語言模型推理的組件,支持 Gemma 2B/7B,Phi-2,Falcon-RW-1B,StableLM-3B 等模型。針對 Gemma 的預轉換模型 (基于 TensorFlow Lite) 可在 Kaggle 下載,并在稍后直接放入 Mediapipe 中加載。

82e4ad24-b223-11ef-93f3-92fbcf53809c.png

LLM Inference

Android Sample

Mediapipe 官方的 LLM Inference Demo 包含了 Android/iOS/Web 前端等平臺。

82f6f0f6-b223-11ef-93f3-92fbcf53809c.png

打開 Android 倉庫會發現幾個特點:

純 Kotlin 實現。

UI 是純 Jetpack Compose 實現。

依賴的 LLM Task SDK 已經高度封裝,暴露出來的方法僅 3 個。

再查看 iOS 的版本:

UI 是 SwiftUI 實現,做的事情和 Compose 一模一樣,稍微再簡化掉一些元素 (例如 Topbar 和發送按鈕)。

依賴的 LLM Task SDK 已經高度封裝,暴露出來的方法一樣為 3 個。

所以,一個好玩的想法出現了:Android 版本的這個 Demo 具備移植到 iOS 上的基礎;移植可使兩邊的代碼高度高度一致,大幅縮減維護成本,而核心要實現的僅僅是橋接下 iOS 上的 LLM Inference SDK。

Kotlin Multiplatform

移植工程所使用的技術叫做 Kotlin Multiplatform (縮寫為 KMP),它是 Kotlin 團隊開發的一種支持跨平臺開發的技術,允許開發者使用相同的代碼庫來構建 Android、iOS、Web 等多個平臺的應用程序。通過共享業務邏輯代碼,KMP 能顯著減少開發時間和維護成本,同時盡量保留每個平臺的原生性能和體驗。Google 在今年的 I/O 大會上也宣布對 KMP 提供一等的支持,把一些 Android 平臺上的庫和工具遷移到了多平臺,KMP 的開發者可以方便的使用它到 iOS 等其他平臺。

盡管 Mediapipe 也支持多個平臺,但我們這次主要聚焦在 Android 和 iOS。一方面更貼近現實,各行各業使用 KMP 的公司的用例更多在移動端上;另外一方面也更方便對標其他移動端開發技術棧。

移植流程

初始化

使用 IDEA 或 Android Studio 創建一個 KMP 的基礎工程,你可以借助 KMP Wizard 或者第三方 KMP App 的模版。如果你沒有 KMP 的相關經驗,可以看到它其實就是一個非常類似 Android 工程的結構,只不過這一次我們把 iOS 的殼工程也放到根目錄,并且在 app 模塊的 build.gradle.kts 內同時配置了 iOS 的相關依賴。

8344db68-b223-11ef-93f3-92fbcf53809c.jpg

封裝和調用 LLM Inference

我們在 commonMain 中,根據 Mediapipe LLM Task SDK 的特征抽象一個簡單的接口,使用 Kotlin 編寫,用以滿足 Android 和 iOS 兩端的需要。該接口取代了原有倉庫里的 InferenceModel.kt 類。

// app/src/commonMain/.../llm/LLMOperator
interface LLMOperator {


    /**
     * To load the model into current context.
     * @return 1. null if it went well 2. an error message in string
     */
    suspend fun initModel(): String?


    fun sizeInTokens(text: String): Int


    suspend fun generateResponse(inputText: String): String


    suspend fun generateResponseAsync(inputText: String): Flow>


}
在 Android 上面,因為 LLM Task SDK 原先就是 Kotlin 實現的,所以除了初始化加載模型文件,其余的部分基本就是代理原有的 SDK 功能。
class LLMInferenceAndroidImpl(private val ctx: Context): LLMOperator {


    private lateinit var llmInference: LlmInference
    private val initialized = AtomicBoolean(false)
    private val partialResultsFlow = MutableSharedFlow>(...)


    override suspend fun initModel(): String? {
        if (initialized.get()) {
            return null
        }
        return try {
            val modelPath = ...
            if (File(modelPath).exists().not()) {
                return "Model not found at path: $modelPath"
            }
            loadModel(modelPath)
            initialized.set(true)
            null
        } catch (e: Exception) {
            e.message
        }
    }
    private fun loadModel(modelPath: String) {
        val options = LlmInference.LlmInferenceOptions.builder()
            .setModelPath(modelPath)
            .setMaxTokens(1024)
            .setResultListener { partialResult, done ->
                // Transforming the listener to flow,
                // making it easy on UI integration.
                partialResultsFlow.tryEmit(partialResult to done)
            }
            .build()


        llmInference = LlmInference.createFromOptions(ctx, options)
    }


    override fun sizeInTokens(text: String): Int = llmInference.sizeInTokens(text)


    override suspend fun generateResponse(inputText: String): String {
        ...
        return llmInference.generateResponse(inputText)
    }


    override suspend fun generateResponseAsync(inputText: String): Flow> {
        ...
        llmInference.generateResponseAsync(inputText)
        return partialResultsFlow.asSharedFlow()
    }


}

而針對 iOS,我們先嘗試第一種調用方式:直接調用 Cocoapods 引入的庫。在 app 模塊引入 cocoapods 的插件,同時添加 Mediapipe 的 LLM Task 庫:

// app/build.gradle.kts
plugins {
    ...
    alias(libs.plugins.cocoapods)
}
cocoapods {
    ...
    ios.deploymentTarget = "15"


    pod("MediaPipeTasksGenAIC") {
        version = "0.10.14"
        extraOpts += listOf("-compiler-option", "-fmodules")
    }
    pod("MediaPipeTasksGenAI") {
        version = "0.10.14"
        extraOpts += listOf("-compiler-option", "-fmodules")
    }
}

注意上面的引入配置中要添加一個編譯參數為 -fmodules 才可正常生成 Kotlin 的引用 (參考鏈接)。

一些 Objective-C 庫,尤其是那些作為 Swift 庫包裝器的庫,在它們的頭文件中使用了 @import 指令。默認情況下,cinterop 不支持這些指令。要啟用對 @import 指令的支持,可以在 pod() 函數的配置塊中指定 -fmodules 選項。

之后,我們在 iosMain 中便可直接 import 相關的庫代碼,如法炮制 Android 端的代理思路:

// 注意這些 import 是 cocoapods 開頭的
import cocoapods.MediaPipeTasksGenAI.MPPLLMInference
import cocoapods.MediaPipeTasksGenAI.MPPLLMInferenceOptions
import platform.Foundation.NSBundle
...
class LLMOperatorIOSImpl: LLMOperator {


    private val inference: MPPLLMInference


        init {
        val modelPath = NSBundle.mainBundle.pathForResource(..., "bin")


        val options = MPPLLMInferenceOptions(modelPath!!)
        options.setModelPath(modelPath!!)
        options.setMaxTokens(2048)
        options.setTopk(40)
        options.setTemperature(0.8f)
        options.setRandomSeed(102)


        // NPE was thrown here right after it printed the success initialization message internally.
        inference = MPPLLMInference(options, null) 
    }


    override fun generateResponse(inputText: String): String {...}
    override fun generateResponseAsync(inputText: String, ...) :... {
        ...
    }
    ...
}

但這回我們沒那么幸運,MPPLLMInference 初始化結束的一瞬間有 NPE 拋出。最可能的問題是因為 Kotlin 現在 interop 的目標是 Objective-C,MPPLLMInference 的構造器比 Swift 版本多一個 error 參數,而我們傳入的是 null。

constructor(
  options: cocoapods.MediaPipeTasksGenAI.MPPLLMInferenceOptions, 
error:CPointer>?)

但幾番測試各種指針傳入,也并未解決這個問題:

// 其中一種嘗試
memScoped {
    val pp: CPointerVar> = allocPointerTo()
    val inference = MPPLLMInference(options, pp.value)
    Napier.i(pp.value.toString())
}

于是只能另辟蹊徑采用第二種方案: 通過 iOS 工程調用第三方庫。

// 1. 聲明一個類似 LLMOperator 的接口但更簡單,方便適配 iOS 的 SDK。
// app/src/iosMain/.../llm/LLMOperator.kt
interface LLMOperatorSwift {
    suspend fun loadModel(modelName: String)
    fun sizeInTokens(text: String): Int
    suspend fun generateResponse(inputText: String): String
    suspend fun generateResponseAsync(
        inputText: String,
        progress: (partialResponse: String) -> Unit,
        completion: (completeResponse: String) -> Unit
    )
}


// 2. 在 iOS 工程里實現這個接口
// iosApp/iosApp/LLMInferenceDelegate.swift
class LLMOperatorSwiftImpl: LLMOperatorSwift {
    ...
    var llmInference: LlmInference?


    func loadModel(modelName: String) async throws {
        let path = Bundle.main.path(forResource: modelName, ofType: "bin")!
        let llmOptions =  LlmInference.Options(modelPath: path)
        llmOptions.maxTokens = 4096
        llmOptions.temperature = 0.9


        llmInference = try LlmInference(options: llmOptions)
    }


    func generateResponse(inputText: String) async throws -> String {
        return try llmInference!.generateResponse(inputText: inputText)
    }


    func generateResponseAsync(inputText: String, progress: @escaping (String) -> Void, completion: @escaping (String) -> Void) async throws {
        try llmInference!.generateResponseAsync(inputText: inputText) { partialResponse, error in
            // progress
            if let e = error {
                print("(self.errorTag) (e)")
                completion(e.localizedDescription)
                return
            }
            if let partial = partialResponse {
                progress(partial)
            }
        } completion: {
            completion("")
        }
    }
    ...    
}


// 3. iOS 再把代理好的(重點是初始化)類傳回給 Kotlin
// iosApp/iosApp/iosApp.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    func application(){
        ...
        let delegate = try LLMOperatorSwiftImpl()
        MainKt.onStartup(llmInferenceDelegate: delegate)        
    }
}


// 4. 最初 iOS 在 KMP 上的實現細節直接代理給該對象(通過構造器注入)
class LLMOperatorIOSImpl(
   private val delegate: LLMOperatorSwift) : LLMOperator {   
   ...
}
細心的朋友可能已經發現,兩端的 Impl 實例需要不同的構造器參數,這個需求一般使用 KMP 的 expect 與 actual 關鍵字解決。下面的代碼中:

利用了 expect class 不需要構造器參數聲明的特點加了層封裝 (類似接口)。

利用了 Koin 實現各自平臺所需參數的注入,再統一把創建的接口實例注入到 Common 層所需的地方。

// Common
expect class LLMOperatorFactory {
    fun create(): LLMOperator
}
val sharedModule = module {
   // 從不同的 LLMOperatorFactory 創建出 Common 層所需的 LLMOperator
  single { get().create() }
}


// Android
actual class LLMOperatorFactory(private val context: Context){
    actual fun create(): LLMOperator = LLMInferenceAndroidImpl(context)
}
val androidModule = module {
    // Android 注入 App 的 Context
    single { LLMOperatorFactory(androidContext()) }
}


// iOS
actual class LLMOperatorFactory(private val llmInferenceDelegate: LLMOperatorSwift) {
    actual fun create(): LLMOperator = LLMOperatorIOSImpl(llmInferenceDelegate)
}


module {
    // iOS 注入 onStartup 函數傳入的 delegate
    single { LLMOperatorFactory(llmInferenceDelegate) }
}
小結: 我們通過一個小小的案例,領略到了 KotlinSwift深度交互。還借助 expect/actual 關鍵字與 Koin 的依賴注入,讓整體方案更流暢和自動化,達到了在 KMP 的 Common 模塊調用 Android 和 iOS Native SDK 的目標。

移植 UI 和 ViewModel

原項目里的 InferenceMode 已經被上一節的 LLMOperator 所取代,因此我們拷貝除 Activity 的剩下 5 個類:

835b8264-b223-11ef-93f3-92fbcf53809c.png

下面我們修改幾處代碼使 Jetpack Compose 的代碼可以方便的遷移到 Compose Multiplatform。

首先是外圍的 ViewModel,KMP 版本我在這里使用了 Voyage,因此替換為 ScreenModel。不過官方 ViewModel 的方案也在實驗中了,請參考這個文檔。

// Android 版本
class ChatViewModel(
    private val inferenceModel: InferenceModel
) : ViewModel() {...}


// KMP 版本,轉換 ViewModel 為 ScreenModel,并修改傳入對象
class ChatViewModel(
    private val llmOperator: LLMOperator
):ScreenModel{...}

Voyage https://github.com/adrielcafe/voyager

文檔 https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodel.html

相應的 ViewModel 初始化方式也更改成 ScreenModel 的方法:

// Android 版本
@Composable
internal fun ChatRoute(
    chatViewModel: ChatViewModel = viewModel(
        factory = ChatViewModel.getFactory(LocalContext.current.applicationContext)
    )
) {
    ...
    ChatScreen(...) {...}
}


// KMP 版本,改成外部初始化后傳入
@Composable
internal fun ChatRoute(
    chatViewModel: ChatViewModel
) {


// 此處采用了默認參數注入的方案,便于解耦。
// koinInject() 是 Koin 官方提供的針對 Compose 
// 的 @Composable 函數注入的一個方法。
@Composable
fun AiScreen(llmOperator:LLMOperator = koinInject()) {
    // 使用 ScreenModel 的 remember 方法
    val chatViewModel = rememberScreenModel { ChatViewModel(llmOperator) }
    ...
    Column {
        ...
        Box(...) {
            if (showLoading) {
                ...
            } else {
                ChatRoute(chatViewModel)
            }
        }
    }
}
對應的 ViewModel 內部的 LLM 功能調用接口也要進行替換:
// Android 版本
inferenceModel.generateResponseAsync(fullPrompt)
inferenceModel.partialResults
    .collectIndexed { index, (partialResult, done) ->
        ...
    }


// KMP 版本,把 Flow 的返回前置了,兼容了兩個平臺的 SDK 設計
llmOperator.generateResponseAsync(fullPrompt)
    .collectIndexed { index, (partialResult, done) ->
        ...
    }

然后是 Compose Multiplatform 特定的資源加載方式,把 R 文件替換為 Res:

// Android 版本
Text(stringResource(R.string.chat_label))


// KMP 版本,該引用是使用插件從 xml 映射而來
// (commonMain/composeResources/values/strings.xml)
import mediapiper.app.generated.resources.chat_label
...
Text(stringResource(Res.string.chat_label))

至此我們已經完成了 ChatScreen ChatViewModel 的主頁面功能遷移。

最后是其他的幾個輕微改動:

LoadingScreen 我們如法炮制傳入 LLMOperator 進行初始化 (替換原有 InferenceModel)。

ChatMessage 只需修改了 UUID 調用的一行 API 到原生實現 (Kotlin 2.0.20 后就不需要了)。

ChatUiState 則完全不用動。

剩下的就只有整體修改下 Log 庫的引用等小細節。

小結: 倘若略去 Log、R 文件的引用替換以及 import 替換等,核心的修改其實僅十幾行,便能把整個 UI 部分也跑起來了

簡單測試

那 Gemma 2B 的性能如何,我們看幾個簡單的例子。此處主要使用三個版本的模型進行測試,模型的定義在 me.xx2bab.mediapiper.llm.LLMOperator (模型在兩端部署請參考項目 README)。

gemma-2b-it-gpu-int4

gemma-2b-it-cpu-int4

gemma-2b-it-cpu-int8

其中:

it 指代一種變體,即 Instruction Tuned 模型,更適合聊天用途,因為它們經過微調能更好地理解指令,并生成更準確的回答。

int4/8 指代模型量化,即將模型中的浮點數轉換為低精度整數,從而減小模型的大小和計算量以適配小型的本地設備例如手機。當然,模型的精度和回答準確度也會有一些下降。

CPU 和 GPU指針對的硬件平臺,這方便了設備 GPU 較弱甚至沒有時可選擇 CPU 執行。從下面的測試結果你會發現當前移動設備上 CPU 版本也常常會占優,因為模型規模小、簡單對話計算操作也不大,并且 Int 量化也有利于 CPU 的指令執行。

首先我們測試一個簡單的邏輯: "蘆筍是不是一種動物"?可以看到下圖的 CPU 版本答案比兩個 GPU (iOS 和 Android) 更合理。而下一個測試是翻譯答案為中文,則是三個嘗試都不太行。

837b13d6-b223-11ef-93f3-92fbcf53809c.jpg

接著我們提高了測試問題的難度,讓它執行區分動植物的單詞分類: 不管是 GPU 或者 CPU 的版本都不錯。

839ab6f0-b223-11ef-93f3-92fbcf53809c.png

再次升級上個問題,讓它用 JSON 的方式輸出答案,就出現明顯的問題:

圖 1 沒有輸出完整的代碼片段,缺少了結尾的三個點 ```。

圖二分類錯誤,把山竹放到動物,植物出現了兩次向日葵。

圖三同二的錯誤,但這三次都沒有純輸出一個 JSON,實際上還是不夠嚴格執行作為 JSON Responder 的角色。

83addb68-b223-11ef-93f3-92fbcf53809c.jpg

最后,這其實不是極限,如果我們使用 cpu-int8 的版本,則可以高準確率地解答上面問題。以及,如果把本 Demo 的 iOS 入口代碼發送給它分析,也能答的不錯。

83c400aa-b223-11ef-93f3-92fbcf53809c.jpg

Gemma 1 的 2B 版本測試至此,我們發覺其推理效果還有不少進步空間,勝在回復速度不錯。而事實上 Gemma 2 的 2B 版本前不久已推出,并且據官方測試其綜合水平已超過 GPT 3.5。這意味著在一臺小小的手機里,本地的推理已經可以達到一年半前的主流模型效果。總結實現這個本地聊天 Demo 的遷移和測試,給了我們些一手的經驗:

LLM 的 On-Device Model 發展非常迅速,而借助 Google 的一系列基礎設施可以讓第三方 Mobile App 開發者也迅速地集成相關的功能,并跨越 Android 與 iOS 雙平臺。

觀望目前情況綜合判斷,LLM 的 On-Device Model 有望在今年達到初步可用狀態,推理速度已經不錯,準確度還有待進一步測試 (例如 Gemma 2 的 2B 版本 + Mediapipe)。

遵循 Android 團隊目前的策略 "Kotlin First"并大膽使用 Compose,是頗具前景的——在基礎設施完備的情況下,一個聊天的小模塊僅寥寥數行修改即可遷移到 iOS。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Google
    +關注

    關注

    5

    文章

    1765

    瀏覽量

    57527
  • 移植
    +關注

    關注

    1

    文章

    379

    瀏覽量

    28130
  • 開源
    +關注

    關注

    3

    文章

    3348

    瀏覽量

    42496
  • iOS
    iOS
    +關注

    關注

    8

    文章

    3395

    瀏覽量

    150605
  • LLM
    LLM
    +關注

    關注

    0

    文章

    288

    瀏覽量

    334

原文標題:【GDE 分享】移植 Mediapipe LLM Demo 到 Kotlin Multiplatform

文章出處:【微信號:Google_Developers,微信公眾號:谷歌開發者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    【算能RADXA微服務器試用體驗】+ GPT語音與視覺交互:1,LLM部署

    。環境變量的配置,未來在具體項目中我們會再次提到。 下面我們正式開始項目。項目從輸入輸出分別涉及了語音識別,圖像識別,LLM,TTS這幾個與AI相關的模塊。先從最核心的LLM開始。 由于LLAMA3
    發表于 06-25 15:02

    怎樣去使用MediaPipe的helloworld example呢

    Mediapipe有何功能?怎樣去使用MediaPipe的helloworld example呢?
    發表于 02-11 07:35

    求助,鴻蒙移植kotlin代碼,需要將其轉換成java實現嗎?

    鴻蒙移植kotlin代碼,需要將其轉換成java實現嗎?
    發表于 06-08 11:33

    求助,官方出的MESH DEMO怎么改成了Kotlin和JAVA混和了?

    對于我們大多數搞偏硬件的,一般都是用C的,對于C++,JAVA有天生的熟悉感,稍微學習一下,在官方的基礎上搞個東西難度不大,但是現在這個Kotlin是個什么鬼?語法規則完全不同了,連分號都不
    發表于 09-21 07:31

    分析Kotlin和Java EE的關系

    java老標準設置的所有障礙。在此過程中,新時代語言Kotlin特定的構造,使的代碼更簡潔而安全。 如果您沒有閱讀本系列的前兩部分,可以在這里找到: Kotlin和Java EE:第一部分 - 從Java
    發表于 09-28 17:12 ?0次下載
    分析<b class='flag-5'>Kotlin</b>和Java EE的關系

    Kotlin的概述

    相信很多開發人員,尤其是Android開發者都會或多或少聽說過Kotlin,當然如果沒有聽過或者不熟悉也沒有關系。因為本篇文章以及博客后期的內容會涉及很多關于Kotlin的知識分享。 在寫
    發表于 09-28 19:48 ?0次下載
    <b class='flag-5'>Kotlin</b>的概述

    基于 MediaPipe 的手語接口現對開發者開放

    客座博文,發布人:SignAll | MediaPipe 團隊 請注意,以下內容中體現的信息、用途及應用完全是 SignAll 客座作者的觀點。 SignAll SDK:使用 MediaPipe
    的頭像 發表于 06-08 18:07 ?2426次閱讀

    使用Kotlin替代Java重構AOSP應用

    兩年前,Android 開源項目 (AOSP) 應用團隊開始使用 Kotlin 替代 Java 重構 AOSP 應用。之所以重構主要有兩個原因: 一是確保 AOSP 應用能夠遵循 Android
    的頭像 發表于 09-16 09:26 ?1869次閱讀
    使用<b class='flag-5'>Kotlin</b>替代Java重構AOSP應用

    bilisoleil-kotlin Kotlin版仿B站項目

    ./oschina_soft/bilisoleil-kotlin.zip
    發表于 06-10 14:12 ?0次下載
    bilisoleil-<b class='flag-5'>kotlin</b> <b class='flag-5'>Kotlin</b>版仿B站項目

    將其Android應用的Java代碼遷移到Kotlin

    J2K,即 IntelliJ/Android Studio 中的 Java Kotlin 轉換器。但 J2K 不是萬能的,遷移中的有些情況仍然很復雜。
    的頭像 發表于 10-28 15:15 ?729次閱讀

    使用Mediapipe控制Gripper

    電子發燒友網站提供《使用Mediapipe控制Gripper.zip》資料免費下載
    發表于 02-06 10:50 ?0次下載
    使用<b class='flag-5'>Mediapipe</b>控制Gripper

    Kotlin發布2023年路線圖:K2編譯器、完善教程文檔等

    Kotlin Multiplatform Mobile:通過提高工具鏈穩定性和文檔,確保兼容性保證,將 Kotlin 移動端技術推向穩定。完善相關生態:借助 Kotklin 庫作者的經驗,整合一批有助于設置、開發和發布
    的頭像 發表于 02-06 10:25 ?711次閱讀

    Kotlin的語法糖解析

    最近又開始要寫一些客戶端代碼,現在項目都是使用Kotlin,但是之前沒有系統的學習過Kotlin,對于Kotlin的一些語法糖還不熟悉,所以寫篇文章總結下。
    的頭像 發表于 04-19 10:21 ?1096次閱讀

    Kotlin聲明式UI框架Compose Multiplatform支持iOS

    JetBrains 在?KotlinConf’23 大會上宣布,Compose Multiplatform 已支持 iOS,目前處于 alpha 階段。至此,Compose
    的頭像 發表于 04-24 09:12 ?1298次閱讀
    <b class='flag-5'>Kotlin</b>聲明式UI框架Compose <b class='flag-5'>Multiplatform</b>支持iOS

    由Java改為 Kotlin過程中遇到的坑

    最近了解了下 Kotlin ,其中的很多語法糖很有意思,并且可以與 Java 無縫兼容。故嘗試在一個 SpringBoot 工程上將部分類修改為 Kotlin ,下面記錄了由 Java 改為
    的頭像 發表于 09-30 16:51 ?814次閱讀
    由Java改為 <b class='flag-5'>Kotlin</b>過程中遇到的坑
    主站蜘蛛池模板: 中文字幕三级| 日本午夜三级| 毛片网站免费在线观看| 亚洲三级视频在线观看| 欧美日韩国产成人高清视频| 一级黄色片a| 91大神在线精品网址| 四虎影院成人| 欧美3d成人动画在线| 天天色天天搞| 国产真实乱xxxav| 天天看影院| 东方天堂网| 日本高清视频色视频kk266| 无夜精品久久久久久| 99久久999久久久综合精品涩| 欧美激情伊人| 日本免费大黄| 深夜免费视频| 被公侵犯肉体中文字幕一区二区| 免费国产一区| 大黄网站在线观看| 五月婷婷狠狠| 亚洲第一视频在线播放| 性久久久久| 激情婷婷网| 在线黄网| 人人做人人爽| 美女被免费视频的网站| 性欧美xxx 不卡视频| 又粗又爽又色男女乱淫播放男女| 又黄又爽又猛大片录像| 免费一看一级毛片| 国产大毛片| 色吧在线观看| 成人拍拍视频| 一区二区三区在线播放| 久久久中文| 欧美五月婷婷| 免费特黄| 激情伦成人综合小说|