主頁 > 移動端開發 > android-- 按需打包的框架搭建--新手教程

android-- 按需打包的框架搭建--新手教程

2021-09-26 09:53:29 移動端開發

1, 新建專案VariantTest

2, 生成keystore

可以看到, 默認的build variant只有debug一種

 

當我試圖選release的時候,發現報錯了

 什么錯呢

 

大致意思是說我們的app沒有簽名

我們知道簽名需要一個keystore, 那么作為一個個人開發者,怎么獲取keystore呢?

studio給我們提供了創建keystore的方式:

  

 

 

 

 

 

現在我們已經有了keystore, 那么下一步就是給專案添加簽名資訊

 

 加完這些以后同步一下, 我們看到已經可以build release app了

 

 以為這就大功告成了嗎? 點擊installRelease,

....幾秒鐘之后, 我得到了一個error

Execution failed for task ':app:installRelease'.
> java.util.concurrent.ExecutionException: com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.example.varianttest signatures do not match the previously installed version; ignoring!

意思是說, 當前試圖安裝的應用(com.example.varianttese)的簽名和之前安裝的不匹配, (因為我之前已經安裝了一個debug app), 雖然這次裝的是release, 但因為沒改包名,所以被認為是同一個app,

這里也體現了android的應用簽名機制,

那就改下包名吧:

如果我們的app只有debug和release兩種, 那么完全可以在buildType/release下面宣告一個不同的applicationId

但是鑒于我們后面還需要添加多個variants, 因此我們新建一個gradle檔案來處理包名-

app_ids.gradle

android.applicationVariants.all { variant ->

    def buildType = variant.buildType.name
    def applicationId = "com.example.varianttest"

    if (buildType.toLowerCase().contains("release")){
        applicationId += ".release"
    }

    variant.mergedFlavor.setApplicationId(applicationId)
}

然后, 在app/build.gradle 檔案頭部去參考它:
apply from: '../app_ids.gradle'

同步一下, 再點擊installRelease, 很快我手機上就有了兩個app-- VariantTest

 這當然是不能接受的, 因為它兩長得一模一樣,我完全分不清,

怎么去改app名字呢? 我們知道app名字定義在manifest中, 所以我們很容易想到新建一個manifest檔案for release

 只需要在src下面新建release目錄, 放入manifest, 完全不需要其他的配置, 編譯release app時就會讀取release目錄目錄下的manifest并和默認manifest合并,

tools: replace的作用就是告訴編譯器,需要將該屬性替換

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.varianttest">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name_release"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VariantTest"
tools:replace="android:label">
</application>

</manifest>

如此操作之后, 我們得到了兩個名字不一樣的app, 同理也可以改app圖示, 這里不再演示,

 

3,從cert/prod的角度構建不同的app

現在為止我們得到debug/release兩個app, 通常來說,debug app不做混淆, 會顯示一些我們需要的log,并且可以斷點除錯

如果我們只是平時自己寫著玩,這個buildType就夠了, 

但是對于絕大多數app來說,都不可避免地要使用網路和server互動,同一條請求,測驗環境和生產環境要用到不同的domain,傳入不同的引數, 或者有的功能我們希望只在測驗app中開放

這個時候老板就希望我們能給build variants加上cert/ production兩種

同時在開發程序中, app端和server端往往同步開工, 那么在api沒有ready的情況下我們也希望有個mock環境能供我們除錯native UI

那就開始搞吧

新建一個gradle檔案-- environment_flavors.gradle: 定義了cert, prod, mock三種環境

android {
    productFlavors {
        cert {
            dimension 'environment'
        }
        production {
            dimension 'environment'
        }
        mock {
            dimension 'environment'
        }
    }
}
然后在app/build.gradle首部添加 
apply from: '../environment_flavors.gradle'

并且申明 flavors: environment:

 

 同步一下, 現在我們已經可以看到這些variants:

 

 我們也希望它們有不同的包名, 這樣我可以在一臺device上同時安裝多個variants

因此我們修改app_ids.gradle, 修改后的代碼如下:(紅色為本次修改的部分)

android.applicationVariants.all { variant ->

def buildType = variant.buildType.name
def applicationId = "com.example.varianttest"
def environmentName = variant.productFlavors[0].name

if (environmentName == "cert") {
applicationId += ".cert"
}
if (environmentName == "production") {
applicationId += ".prod"
}
if (environmentName == "mock") {
applicationId += ".mock"
}

if (buildType.toLowerCase().contains("release")){
applicationId += ".release"
}

variant.mergedFlavor.setApplicationId(applicationId)
}

包名不同保證了我們可以同時安裝, 此外我們也希望這些app有不同的名字,否則裝在一起我們完全不知道誰是誰

這時我們已經不大可能為每個variant都去創建一個manifest了, 怎么辦呢?我們可以使用占位符來解決

manifest檔案中:

android:label="@string/app_name${appNameEnv}${appNameBuildType}"

再修改app/build.gradle:

defauleConfig{
...
    manifestPlaceholders = [appNameEnv: "", appNameBuildType: ""]
}
buildTypes {
release {
...
manifestPlaceholders.appNameBuildType = '_release'
}
}

然后 environment_flavors.gradle:

android {
productFlavors {
cert {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_cert'
}
production {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_prod'
}
mock {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_mock'
}
}
}

同步一下, 現在我們已經可以得到6個build了 

 

但是到目前為止, cert/prod/mock的內容完全一樣,根本體現不出應有的價值,那么接下來就是最關鍵的操作了, 怎么讓不同的build去關聯不同的環境呢?

我們很容易想到通過BuildConfig在代碼中獲取到當前的build flavors, 然后可以據此判斷,設定不同的環境,如下面的代碼:

 

 當不同環境之間只有極少數區別且不涉及頻繁改動的時候, 這種方式當然也可以, 但缺點是耦合性太高,不利于后期的維護和擴展

因此在專案中, 我更偏向于使用一個json檔案來描述不同的配置, 比如我們之前在environment_flavors.gradle中宣告了3種flavors: cert/mock/production

那么對應的,我們可以在app/src下面創建3個assets檔案夾,分別放入apiConfig.json

 apiConfig.json (mock和production中host的值分別對應.mock和.production)

 定義data class ApiConfiguration

data class ApiConfiguration(
val host: String
)

創建一個工具類讀取assets中的json檔案并轉換為ApiConfiguration物件

interface AssetsLoader {

fun getApiConfiguration() : ApiConfiguration
}

class ApplicationAssetsLoader(private val configLoader: ConfigurationLoader) : AssetsLoader {

override fun getApiConfiguration(): ApiConfiguration {
return loadConfig("apiConfig.json")
}

private inline fun <reified T : Any> loadConfig(fileName: String): T {
return configLoader.requireConfig(fileName)
}
}


interface ConfigurationLoader {
fun <T : Any> loadConfig(fileName: String, type: KClass<T>): T?
}

inline fun <reified T : Any> ConfigurationLoader.requireConfig(
fileName: String
): T {
return loadConfig(fileName, T::class)
?: throw IllegalStateException("$fileName config file does not exist")
}

class JsonConfigurationLoader(
val gson: Gson,
val assets: AssetManager
) : ConfigurationLoader {

override fun <T : Any> loadConfig(fileName: String, type: KClass<T>): T? {
return try {
BufferedReader(InputStreamReader(assets.open(fileName)))
.use { reader -> gson.fromJson(reader, type.java) }
} catch (e: IOException) { // Exception is thrown if file is missing or couldn't be read
null
}
}
}

 這里其實可以寫得很簡單, 本例中因為考慮到后面不同variants可能還要讀取一些不同的檔案型別, 所以抽象出了介面,

因為我們之前在app/build.gradle中已經宣告了:

flavorDimensions 'environment'

 所以只要上面我們新建的那三個檔案夾的名字和environment_flavors.gradle中宣告的一致,就不再需要其他的任何配置, 每個buildVariants都可以讀到正確的json檔案

簡單測驗一下,代碼如下

 

  

 本例中使用了MVVM, 資料驅動UI,分別跑一下cert/mock/prod app, 可以看到它們都拿到了正確的環境配置

 

4, Mock環境搭建

看到這里, 聰明的小伙伴們肯定會有個疑問, mock環境通常供開發者除錯ui使用, 并不涉及和api的互動, 所以自然也就不需要api domain之類的東西

那么怎么實作mock呢?

比如,現在我們有一條網路請求getMoney,要去server拿response顯示在home頁面

于是我們根據api同事預先提供的回傳資料格式寫了資料類

data class GetMoneyResponse(
val name: String,
val count: Int,
val type: String,
val currency: String
)

介面GetMoneyRepository:

interface GetMoneyRepository {
fun getMoney(): GetMoneyResponse
}

介面實作類:

class GetMoneyRepositoryImpl() : GetMoneyRepository {
override fun getMoney(): GetMoneyResponse {
//這里應該要去call api
//本例省去了這個步驟
return GetMoneyResponse("name", 0, "type", "currency")
}
}

然后在viewModel中呼叫

private val _response = MutableLiveData<GetMoneyResponse>().apply {
value = https://www.cnblogs.com/haigs/p/GetMoneyRepositoryImpl().getMoney()
}

val response: LiveData<GetMoneyResponse> = _response

在fragment 顯示

private fun getData(){
homeViewModel.response.observe(viewLifecycleOwner, {
responseView.text = it.name + "通過:" + it.type + "賺到了:"+ it.count + it.currency

})
}

至此, native部分就寫完了,可是在api遲遲沒有ready的情況下, 我們怎么用mock資料來測驗呢?

上文中, 我們已經為mock環境創建了mock檔案夾,并放入了mock build會用到的assets檔案

現在我們在該檔案夾下新建兩個子目錄with和without 

將類GetMoneyRepositoryImpl移到without目錄下, 我們希望真實環境(cert/prod)下可以編譯這個檔案

然后在with目錄下再創建一個GetMoneyRepositoryImpl供mock環境使用

class GetMoneyRepositoryImpl() : GetMoneyRepository {

override fun getMoney(): GetMoneyResponse {
//因為這個類供mock使用, 因此我們可以直接回傳我們想要的任何response
//通常的做法是在mock/assets下加入我們想要的response檔案,如 getMoneyResponse.json, 然后讀取assets
//本例中簡化了這一步
return GetMoneyResponse("張三", 500, "搬磚", "人民幣")
}

}
所以現在的目錄就變成了這樣

 注意, 這里的兩個實作類GetMoneyRepositoryImpl擁有完全相同的類名和包名,只是方法實作不同

因此, 我們會發現viewModel里面報錯了, 因為編譯器不允許同時存在兩個一樣的類

所以下一步,我們就需要告訴編譯器,什么時候該用哪個類

在app/build.gradle 下面添加如下描述:

android {
String mockSources = "src/mock/with"
String noMockSources = "src/mock/without"
sourceSets {
main {
java.srcDirs += ['src/main/kotlin']
}
cert {
java.srcDirs += [noMockSources]
}
mock {
java.srcDirs += [mockSources]
}
production {
java.srcDirs += [noMockSources]
}
}
}

這段的作用就是告訴編譯器,mock環境就編譯“src/mock/with”下面的代碼, 否則就編譯“src/mock/without”下的代碼

大功告成, 我們分別安裝cert和mock app驗證一下:

 

 5, 多維變體

 就當我覺得可以松一口氣的時候,老板又提出了新需求, 隨著公司業務的不斷擴展, 我們的app在全球范圍內都有了客戶群,各種風格/功能上的差異已經不僅僅是改改copy就能解決的了,所以老板希望我們能再增加一個國家的維度, 給不同的國家提供不通的app 

本質上講, 這和上文說到的environment變體并沒有什么不同, 只是新增一個維度而已,  下面我們來看具體實作

新建country_flavors.gradle, 為了簡單,我們只宣告了china和uk兩個國家

android {
productFlavors {
china {
dimension 'country'
}
uk {
dimension 'country'
}
}
}

在app/build.gradle中參考這個檔案

apply from: '../country_flavors.gradle'

并修改flavorDimensions, 增加country維度

flavorDimensions 'country', 'environment'

修改app_ids.gradle,讓不同的國家擁有不同的包名

android.applicationVariants.all { variant ->

    def buildType = variant.buildType.name
    def countryName = variant.productFlavors[0].name.toUpperCase()
    def environmentName = variant.productFlavors[1].name
    def appIdCountry = AppId.valueOf(countryName)
    def applicationId = appIdCountry.appId

    if (environmentName == "cert") {
        applicationId += appIdCountry.certSuffix
    }else if (environmentName == "production") {
        applicationId += appIdCountry.productionSuffix
    } else if (environmentName == "mock") {
        applicationId += appIdCountry.mockSuffix
    }

    if (buildType.toLowerCase().contains("release")){
        applicationId += appIdCountry.RELEASE_SUFFIX
    }

    variant.mergedFlavor.setApplicationId(applicationId)
}

enum AppId {
    CHINA("com.variant.china"),
    UK("com.variant.uk")

    public final String appId
    private final static String MOCK_SUFFIX = ".mock"
    private final static String CERT_SUFFIX = ".cert"
    public final static String RELEASE_SUFFIX = ".release"
    private final static String PROD_SUFFIX = ".prod"
    public final String certSuffix
    public final String mockSuffix
    public final String productionSuffix

    AppId(String appId, String certSuffix = CERT_SUFFIX, String prodSuffix = PROD_SUFFIX, String mockSuffix = MOCK_SUFFIX) {
        this.appId = appId
        this.certSuffix = certSuffix
        this.mockSuffix = mockSuffix
        this.productionSuffix = prodSuffix
    }
}

同時, 在app/src目錄下新建china/res/values/strings.xml :

<resources>
    <string name="app_name_cert">Variant China Cert Debug</string>
    <string name="app_name_cert_release">Variant China Cert Release</string>
    <string name="app_name_mock">Variant China Mock Debug</string>
    <string name="app_name_mock_release">Variant China Mock Release</string>
    <string name="app_name_prod">Variant China Prod Debug</string>
    <string name="app_name_prod_release">Variant China Prod Release</string>
    <string name="title_home">主頁</string>
    <string name="title_dashboard">活動</string>
    <string name="title_notifications">通知</string>
</resources>

  和 uk/res/values/strings.xml: 

<resources>
    <string name="app_name_cert">Variant UK Cert Debug</string>
    <string name="app_name_cert_release">Variant UK Cert Release</string>
    <string name="app_name_mock">Variant UK Mock Debug</string>
    <string name="app_name_mock_release">Variant UK Mock Release</string>
    <string name="app_name_prod">Variant UK Prod Debug</string>
    <string name="app_name_prod_release">Variant UK Prod Release</string>
    <string name="title_home">Home</string>
    <string name="title_dashboard">Dashboard</string>
    <string name="title_notifications">Notifications</string>
</resources>

  這樣,不同的app也可以讀到不同的copy,顯示不同的包名

注意這里和android 的copy 國際化不太一樣, 沒有根據local來確定copy, 而是根據我們自己設定的build variant, 處理更加靈活

 

6, 現在我們已經可以從country的維度來build出不同的app了, 那么接下來, 怎么讓不同的country有不同的功能呢?

類似于上文第4步, 在app/src/china以及app/src/uk目錄下新建assets檔案夾, 加入featureConfig.json (China配置為true, uk配置false)

{
  "showImage": true
} 

我們根據該config來決定要不要顯示首頁的一張圖片

private fun initImageView(){
        homeViewModel.showImage.observe(viewLifecycleOwner, {showImage ->
            if (showImage){
                imageView.visibility = View.VISIBLE
            } else {
                imageView.visibility = View.GONE
            }
        })
        homeViewModel.getFeatureConfiguration(requireContext())
    }

json檔案的讀取也與第4步相似,不再贅述,我們直接來看結果, 下圖中左邊是china, 右邊是uk

 

 

 

7,  按需打包

看到這里, 我們就掌握了多維app構建的基本方法,當然我們還可以增加更多的維度,比如按應用市場, baidu/huawei/xiaomi 等等, 但基本原理都是一樣的,

然而,就當我準備關電腦下班時, 老板又找到了我, 提出了新需求:

在我等加班??幾年的努力下, 我們的app功能不斷增多, 引入了大量的第三方庫, 導致的結果就是app size不斷增大, 眼看就要突破google設定的150M生死線, 所以給app瘦身就成了當前迫在眉睫的問題,

經粗略統計, 我們共引入了幾十個第三方庫, 但是并非所有的app都需要這些庫, 所以我們應該通過country來配置依賴

這里我們以 okhttp為例, 假設china 需要okhttp在首頁加載一張圖片, 但uk全程都不需要

那我們先新建country_implementations.gradle檔案, 并在app/build.gradle中參考

dependencies {
    chinaImplementation "com.squareup.okhttp3:okhttp:4.4.0"
    chinaImplementation "com.squareup.okhttp3:okhttp-urlconnection:4.4.0"
}

注意這里的 chinaImplementation  意思就是只給china 添加依賴, 

不用擔心編譯器找不到這個方法, 因為我們之前已經宣告了名為 country的flavor, 包括了china和uk

所以編譯器完全可以識別這個命令, 就跟我們平常用的testImplementation, debugImplementation一樣

 

接下來就是怎么呼叫的問題了, 以前我們直接在整個工程下添加依賴, 這樣專案里的任何地方都可以獲取到該依賴

但現在,因為我們只給china 加了,所以不能在工程代碼里直接呼叫,否則,當你build uk app時根本找不到

比如,當我build uk variant時, 這行代碼是報錯的

怎么解決呢?

其實思路和上文中搭建mock環境一樣

 

 

 在src下新建journey/okhttp/main/java目錄, 分別放入兩個同名的工具類HttpJourney

在with/main/java目錄下的檔案里, 我們實作了我們要用到的http journey的一些方法

在without/main/java目錄下的檔案里, 只需要定義空方法,或者直接throw exception

然后在fragment里呼叫

private fun initImageView(){
        homeViewModel.showImage.observe(viewLifecycleOwner, {showImage ->
            if (showImage){
                HttpJourney().getImageViaHttp()
                imageView.visibility = View.VISIBLE
            } else {
                imageView.visibility = View.GONE
            }
        })
        homeViewModel.getFeatureConfiguration(requireContext())
    } 

注意這里 showImage 在uk config里的配置一定是false .

代碼部分加完了, 最后一步就是告訴編譯器, china和uk分別要編譯哪些journey的代碼 

我們繼續在country_implementations.gradle中添加如下代碼 

enum Journey {
    HTTP_JOURNEY("okhttp")

    public final String sourceSetName

    Journey(String sourceSetName) {
        this.sourceSetName = sourceSetName
    }
}

static def addJourneySources(String country, List<Journey> journeys, sourceSets) {
    def included = Journey.values().toList().intersect(journeys)
    def excluded = Journey.values() - included
    def sourceSet = sourceSets.findByName(country)

    for (journey in included) {
        sourceSet.java.srcDirs += "src/journey/${journey.sourceSetName}/with/main/java"
    }
    for (journey in excluded) {
        sourceSet.java.srcDirs += "src/journey/${journey.sourceSetName}/without/main/java"
    }
}

android {
    sourceSets { container ->
        china {
            ArrayList journeys = new ArrayList([Journey.HTTP_JOURNEY])
            addJourneySources(name, journeys, container)
        }
        uk {
            ArrayList journeys = new ArrayList([])
            addJourneySources(name, journeys, container)
        }
    }
}  

代碼很簡單,相信大家都能看懂, 這里就不廢話了

看看結果,分別build和china和uk的cert app

 

 umm, 效果還是有的??


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

標籤:Android

上一篇:Android從入門到進階之高級控制元件

下一篇:OC原始碼剖析物件的本質

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