docs(android): 完整方案 + logo 资源 + 启动屏
新增 docs/android/ 目录: - README.md 总入口(快速上手 + 决策摘要 + 数据流) - 01-architecture.md 模块划分 + 数据流 + 选型理由 - 02-api-contract.md 每个接口的请求/响应 + DTO 字段映射 - 03-build-run.md Gradle/SDK/网络安全白名单/真机调试 - 04-milestones.md 7 天里程碑 + DoD + E2E 测试场景 新增 assets/: - logo/: 主图标 master + adaptive icon + 5 DPI launcher (方/圆) - splash/: 启动屏 logo + 完整背景预览 + 5 DPI 资源 - android_resources/: 集成所需的 XML(adaptive icon/主题/颜色/字符串/drawable/layout) - INTEGRATION.md 集成指南 - logo.svg + _make_logo.py 设计源 设计风格:参考用户提供的木质方块字母积木图,米色木纹底 + 深棕色字母 D,代表 'Diary',温暖私人日记感。 服务器体检:所有容器/API/DB/翻译主链路正常,TMT 本月已用 0.37%。 MaaS 备用通道上次已验证可用。
This commit is contained in:
602
docs/android/03-build-run.md
Normal file
602
docs/android/03-build-run.md
Normal file
@@ -0,0 +1,602 @@
|
||||
# 03 · 构建与运行
|
||||
|
||||
> 从 0 到能装到真机跑的 APK,每一步都给具体命令。
|
||||
>
|
||||
> 假设你本地已经有:**Android Studio Hedgehog(2023.1.1)+** + **JDK 17** + **Android SDK 35** + **Kotlin 2.0.21**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 环境清单
|
||||
|
||||
| 工具 | 版本 | 验证命令 |
|
||||
|---|---|---|
|
||||
| Android Studio | Hedgehog (2023.1.1) 或更新 | Help → About |
|
||||
| JDK | 17 | `java -version` |
|
||||
| Android SDK | 35 | SDK Manager 看 "Android 15.0" |
|
||||
| Gradle | 8.10+(Studio 自带) | `gradle --version` |
|
||||
| Kotlin | 2.0.21(项目自带)| 无所谓 |
|
||||
|
||||
**JDK 注意**:**JDK 17**,不是 11、不是 21。AGP 8.7 要求 JDK 17。
|
||||
|
||||
---
|
||||
|
||||
## 1. 新建工程
|
||||
|
||||
1. **File → New → New Project**
|
||||
2. 选 **Empty Activity (Compose)**
|
||||
3. 配置:
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| Name | `Diary News` |
|
||||
| Package name | `com.diary.news` |
|
||||
| Save location | `~/projects/diary-news-android/` |
|
||||
| Language | **Kotlin** |
|
||||
| Minimum SDK | **API 24 ("Nougat")** |
|
||||
| Build configuration language | **Kotlin DSL** |
|
||||
| ☑️ Use Version Catalog | **勾上**(用 `libs.versions.toml`)|
|
||||
|
||||
4. **Finish**
|
||||
|
||||
---
|
||||
|
||||
## 2. 改版本目录
|
||||
|
||||
打开 `gradle/libs.versions.toml`,**整体替换**为:
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
agp = "8.7.2"
|
||||
kotlin = "2.0.21"
|
||||
ksp = "2.0.21-1.0.27"
|
||||
|
||||
# AndroidX & Compose
|
||||
compose-bom = "2024.10.01"
|
||||
activity-compose = "1.9.3"
|
||||
lifecycle = "2.8.7"
|
||||
navigation-compose = "2.8.4"
|
||||
|
||||
# Network
|
||||
retrofit = "2.11.0"
|
||||
retrofit-kotlinx-converter = "1.0.0"
|
||||
okhttp = "4.12.0"
|
||||
kotlinx-serialization = "1.7.3"
|
||||
kotlinx-coroutines = "1.9.0"
|
||||
|
||||
# DI
|
||||
hilt = "2.52"
|
||||
hilt-navigation-compose = "1.2.0"
|
||||
|
||||
# Persistence
|
||||
room = "2.6.1"
|
||||
security-crypto = "1.1.0-alpha06"
|
||||
|
||||
# Paging
|
||||
paging = "3.3.4"
|
||||
paging-compose = "3.3.4"
|
||||
|
||||
# Image
|
||||
coil = "2.7.0"
|
||||
|
||||
# Test
|
||||
junit = "4.13.2"
|
||||
mockk = "1.13.13"
|
||||
turbine = "1.2.0"
|
||||
|
||||
[libraries]
|
||||
# Core
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
|
||||
# Compose
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
|
||||
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
|
||||
|
||||
# Network
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit-kotlinx-serialization = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-converter" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
|
||||
# DI
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
|
||||
|
||||
# Persistence
|
||||
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
|
||||
security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "security-crypto" }
|
||||
|
||||
# Paging
|
||||
paging-runtime = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" }
|
||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging-compose" }
|
||||
|
||||
# Image
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
|
||||
# Test
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 改 `app/build.gradle.kts`
|
||||
|
||||
整体替换为:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.diary.news"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.diary.news"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
// debug 阶段先不上签名,直接装 debug APK
|
||||
// 真要 release,见 §6
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
// Network
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.kotlinx.serialization)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// DI
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
|
||||
// Persistence
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
implementation(libs.room.paging)
|
||||
ksp(libs.room.compiler)
|
||||
implementation(libs.security.crypto)
|
||||
|
||||
// Paging
|
||||
implementation(libs.paging.runtime)
|
||||
implementation(libs.paging.compose)
|
||||
|
||||
// Image
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
// Test
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.turbine)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 网络安全白名单(关键!)
|
||||
|
||||
### 4.1 `app/src/main/AndroidManifest.xml`
|
||||
|
||||
整体替换:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".DiaryNewsApp"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.DiaryNews"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="35"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.DiaryNews">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
### 4.2 `app/src/main/res/xml/network_security_config.xml`
|
||||
|
||||
新建文件:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- 默认所有域走系统 HTTPS(拒绝明文)-->
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<!-- 例外:只对后端 server 放行明文 HTTP -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">207.57.129.228</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
```
|
||||
|
||||
### 4.3 `app/src/main/res/xml/backup_rules.xml`(禁止备份 token)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="auth_prefs.xml" />
|
||||
</full-backup-content>
|
||||
```
|
||||
|
||||
### 4.4 `app/src/main/res/xml/data_extraction_rules.xml`(Android 12+)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="sharedpref" path="auth_prefs.xml" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="sharedpref" path="auth_prefs.xml" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. ProGuard / R8 规则(发布包必加)
|
||||
|
||||
`app/proguard-rules.pro`:
|
||||
|
||||
```proguard
|
||||
# Retrofit
|
||||
-keepattributes Signature, InnerClasses, EnclosingMethod
|
||||
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
|
||||
-keepattributes AnnotationDefault
|
||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
|
||||
# kotlinx.serialization
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt
|
||||
|
||||
-keep,includedescriptorclasses class com.diary.news.**$$serializer { *; }
|
||||
-keepclassmembers class com.diary.news.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class com.diary.news.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# OkHttp
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
# Hilt
|
||||
-keep class dagger.hilt.** { *; }
|
||||
-keep class * extends dagger.hilt.android.HiltAndroidApp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 第一次 Sync & Build
|
||||
|
||||
### 6.1 Sync Gradle
|
||||
|
||||
- Android Studio 顶部会弹"Gradle files have changed..." → **Sync Now**
|
||||
- 或菜单 **File → Sync Project with Gradle Files**
|
||||
- 等 5-15 分钟(下载依赖)
|
||||
|
||||
### 6.2 第一次 build
|
||||
|
||||
```bash
|
||||
# 命令行
|
||||
./gradlew assembleDebug
|
||||
|
||||
# 或 Studio 右上角 ▶️ 直接 Run
|
||||
```
|
||||
|
||||
预期:
|
||||
- `BUILD SUCCESSFUL`
|
||||
- 产物在 `app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
### 6.3 跑在模拟器
|
||||
|
||||
1. 工具栏 AVD Manager → Create Virtual Device → Pixel 7 + API 35
|
||||
2. 启动模拟器(等 30s-1min)
|
||||
3. Run ▶️
|
||||
|
||||
**模拟器坑**:
|
||||
- 模拟器自己的 IP 是 `10.0.2.2`,不是 `127.0.0.1`
|
||||
- 但我们的目标是 `207.57.129.228`(真实服务器),**模拟器直连外网就行,无需特殊配置**
|
||||
- 如果你只是想测本地后端,临时把 `network_security_config.xml` 改成 `10.0.2.2` 白名单
|
||||
|
||||
### 6.4 跑在真机
|
||||
|
||||
1. **设置 → 关于手机 → 连续点 7 次"版本号"** → 开启开发者模式
|
||||
2. **设置 → 系统 → 开发者选项 → 打开 USB 调试**
|
||||
3. USB 连电脑 → 手机弹"允许 USB 调试" → 确定
|
||||
4. Studio 右上角设备列表选你的真机 → Run ▶️
|
||||
|
||||
---
|
||||
|
||||
## 7. 配置服务器地址(可切换 debug / release)
|
||||
|
||||
**MVP 写死在 `BuildConfig`**。
|
||||
|
||||
在 `app/build.gradle.kts` 的 `defaultConfig` 加:
|
||||
|
||||
```kotlin
|
||||
defaultConfig {
|
||||
// ...
|
||||
buildConfigField("String", "API_BASE_URL", "\"http://207.57.129.228:3000/api/v1/\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
buildConfigField("String", "API_BASE_URL", "\"http://207.57.129.228:3000/api/v1/\"")
|
||||
}
|
||||
release {
|
||||
buildConfigField("String", "API_BASE_URL", "\"http://207.57.129.228:3000/api/v1/\"")
|
||||
// 后续要换 https,改这里 + 加证书
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
代码里用:
|
||||
|
||||
```kotlin
|
||||
@Provides @Singleton
|
||||
fun provideRetrofit(okHttp: OkHttpClient): Retrofit =
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.client(okHttp)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 调试技巧
|
||||
|
||||
### 8.1 OkHttp 日志
|
||||
|
||||
```kotlin
|
||||
@Provides @Singleton
|
||||
fun provideOkHttp(
|
||||
tokenStore: TokenStore,
|
||||
): OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(AuthInterceptor(tokenStore))
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE
|
||||
})
|
||||
.authenticator(TokenAuthenticator(tokenStore, ...))
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
```
|
||||
|
||||
`Level.BODY` 会在 Logcat 打印所有请求和响应体(包含 token!小心)。
|
||||
|
||||
**生产包务必改回 `Level.NONE`**。
|
||||
|
||||
### 8.2 Compose Preview
|
||||
|
||||
```kotlin
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ArticleCardPreview() {
|
||||
DiaryNewsTheme {
|
||||
ArticleCard(
|
||||
a = Article(
|
||||
id = 1,
|
||||
sourceName = "DW",
|
||||
sourceSlug = "dw",
|
||||
title = "Sample title",
|
||||
titleZh = "示例标题",
|
||||
bodyZhText = "示例正文...",
|
||||
...
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Layout Inspector
|
||||
|
||||
Studio → **Tools → Layout Inspector** → 选进程 → 实时看 Compose 树。
|
||||
|
||||
### 8.4 抓包
|
||||
|
||||
推荐 **Charles** 或 **mitmproxy**:
|
||||
- 模拟器:WiFi → 长按 → 修改网络 → 手动代理 → 127.0.0.1:8888
|
||||
- 真机:Charles Proxy + 安装证书
|
||||
- 注意:**mitmproxy 会让 HTTPS 失效**(因为加了第三方 CA),我们的 HTTP 不受影响
|
||||
|
||||
---
|
||||
|
||||
## 9. CI(可选,MVP 不强求)
|
||||
|
||||
如果你想每次 push 自动出 debug APK:
|
||||
|
||||
`.github/workflows/build.yml`:
|
||||
|
||||
```yaml
|
||||
name: Build APK
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-debug
|
||||
path: app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见报错速查
|
||||
|
||||
| 报错 | 原因 | 解决 |
|
||||
|---|---|---|
|
||||
| `Cleartext HTTP traffic to 207.57.129.228 not permitted` | 没配 network_security_config | 装 §4 |
|
||||
| `Cannot resolve symbol 'hiltViewModel'` | 没加 `hilt-navigation-compose` | 装 §2 依赖 |
|
||||
| `Hilt: @AndroidEntryPoint ... missing binding` | Application 没 `@HiltAndroidApp` | 检查 `DiaryNewsApp.kt` |
|
||||
| `KSP not found` | KSP 版本与 Kotlin 不匹配 | 检查 `ksp` 版本号 |
|
||||
| `Plugin [id: 'com.google.devtools.ksp'] was not found` | settings.gradle.kts 没声明 | 在 `pluginManagement` 块加 |
|
||||
| `Composable invocations can only happen from the context of a @Composable function` | 在 `LaunchedEffect` 里调非 suspend 阻塞 | 加 `withContext(Dispatchers.IO)` |
|
||||
| `JSON decode error: Polymorphic serializer was not found` | 后端返回了 polymorphic 类型,DTO 没声明 | DTO 加 `@JsonClassDiscriminator` 或 sealed class |
|
||||
| `401 Unauthorized` 一片 | Token 过期 / 错 | 启动 app 看 logcat 的 OkHttp 日志,确认 refresh 接口通不通 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 发布签名(真要上架时再弄)
|
||||
|
||||
```bash
|
||||
# 生成 keystore
|
||||
keytool -genkey -v -keystore diary-news.jks -keyalg RSA -keysize 2048 \
|
||||
-validity 10000 -alias diary-news
|
||||
|
||||
# 在 ~/.gradle/gradle.properties 加
|
||||
DIARY_NEWS_STORE_FILE=diary-news.jks
|
||||
DIARY_NEWS_STORE_PASSWORD=...
|
||||
DIARY_NEWS_KEY_ALIAS=diary-news
|
||||
DIARY_NEWS_KEY_PASSWORD=...
|
||||
|
||||
# app/build.gradle.kts
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(providers.gradleProperty("DIARY_NEWS_STORE_FILE").get())
|
||||
storePassword = providers.gradleProperty("DIARY_NEWS_STORE_PASSWORD").get()
|
||||
keyAlias = providers.gradleProperty("DIARY_NEWS_KEY_ALIAS").get()
|
||||
keyPassword = providers.gradleProperty("DIARY_NEWS_KEY_PASSWORD").get()
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**MVP 阶段不弄**,直接 debug APK 装手机上跑就够了。
|
||||
Reference in New Issue
Block a user