Android compose 的基本环境搭建
1.创建项目
导入版本
1.gradle/libs.versions.toml
[versions]
accompanistPermissions = "0.36.0"
agp = "8.5.0-beta01"
coilCompose = "2.7.0"
constraintlayoutComposeVersion = "1.0.1"
hiltAndroid = "2.51.1"
hiltNavigationCompose = "1.2.0"
kotlin = "1.9.25"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
activityCompose = "1.9.2"
composeBom = "2024.09.01"
kotlinxSerializationJson = "1.6.3"
lifecycleViewmodelKtx = "2.8.5"
composeRuntime = "1.7.1"
lingver = "1.3.0"
loggingInterceptorVersion = "4.12.0"
navigationCompose = "2.8.0"
retrofit = "2.11.0"
roomRuntime = "2.6.1"
rxandroid = "2.1.1"
rxandroidVersion = "3.0.1"
rxjava = "2.2.21"
rxjava3 = "3.1.9"
rxlifecycle = "3.1.0"[libraries]androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
#region viewmodel livedata compose
androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycleViewmodelKtx" }
constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutComposeVersion" }
runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "composeRuntime" }
androidx-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeRuntime" }
androidx-runtime-rxjava2 = { module = "androidx.compose.runtime:runtime-rxjava2" }
#endregion compose#region kotlin serialization
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
#endregion#region hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
#endregion#region navigation
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
#endregion#region retrofit2 and okhttp3
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptorVersion" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }#endregion okhttp3#region rxjava 2
adapter-rxjava2 = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" }
rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxandroid" }
rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" }
#endregion#region rxjava 3
rxjava3-rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroidVersion" }
rxjava3-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava3" }
adapter-rxjava3 = { module = "com.squareup.retrofit2:adapter-rxjava3", version.ref = "retrofit" }
rxlifecycle = { module = "com.trello.rxlifecycle3:rxlifecycle", version.ref = "rxlifecycle" }
rxlifecycle-android-lifecycle-kotlin = { module = "com.trello.rxlifecycle3:rxlifecycle-android-lifecycle-kotlin", version.ref = "rxlifecycle" }
rxlifecycle-components = { module = "com.trello.rxlifecycle3:rxlifecycle-components", version.ref = "rxlifecycle" }#endregion#region room
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }#endregion#region coil
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
#endregion#region accompanist
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
#endregion#region 国际化
lingver = { module = "com.github.YarikSOffice:lingver", version.ref = "lingver" }
#endregionjunit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
我整理好的版本,都用过了适配
2.模块目录下的 build.gradle.kts
plugins {alias(libs.plugins.android.application)alias(libs.plugins.jetbrains.kotlin.android)id("kotlin-kapt")id("com.google.dagger.hilt.android")kotlin("plugin.serialization")id("androidx.room")
}android {namespace = "com.composeapp"compileSdk = 34defaultConfig {applicationId = "com.composeapp"minSdk = 24targetSdk = 34versionCode = 1versionName = "1.0"testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"vectorDrawables {useSupportLibrary = true}}room {schemaDirectory("$projectDir/schemas")}buildTypes {release {isMinifyEnabled = falseproguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),"proguard-rules.pro")}}compileOptions {sourceCompatibility = JavaVersion.VERSION_1_8targetCompatibility = JavaVersion.VERSION_1_8}kotlinOptions {jvmTarget = "1.8"}buildFeatures {compose = true}composeOptions {kotlinCompilerExtensionVersion = "1.5.15"}packaging {resources {excludes += "/META-INF/{AL2.0,LGPL2.1}"}}}dependencies {implementation(libs.androidx.core.ktx)implementation(libs.androidx.activity.compose)implementation(platform(libs.androidx.compose.bom))implementation(libs.androidx.ui)implementation(libs.androidx.ui.graphics)implementation(libs.androidx.ui.tooling.preview)implementation(libs.androidx.material3)testImplementation(libs.junit)androidTestImplementation(libs.androidx.junit)androidTestImplementation(libs.androidx.espresso.core)androidTestImplementation(platform(libs.androidx.compose.bom))androidTestImplementation(libs.androidx.ui.test.junit4)debugImplementation(libs.androidx.ui.tooling)debugImplementation(libs.androidx.ui.test.manifest)implementation(libs.kotlinx.serialization.json)//region ViewModel Livedata compose runtimeimplementation(libs.androidx.lifecycle.viewmodel.ktx)implementation(libs.androidx.lifecycle.viewmodel.compose)implementation(libs.androidx.lifecycle.livedata.ktx)implementation(libs.androidx.lifecycle.runtime.ktx)implementation(libs.androidx.lifecycle.lifecycle.runtime.compose)implementation(libs.androidx.lifecycle.viewmodel.savedstate)implementation(libs.androidx.runtime)implementation(libs.runtime.livedata)//endregion// region hiltimplementation(libs.hilt.android)kapt(libs.hilt.android.compiler)implementation(libs.androidx.hilt.navigation.compose)// endregion//region navigationimplementation(libs.androidx.navigation.compose)//endregion navigation//region retrofit2 okhttp//Retrofit 核心库implementation(libs.retrofit)//响应数据自动序列化//JSONimplementation(libs.converter.gson)//String类型implementation(libs.converter.scalars)//拦截器 loggingimplementation(libs.okhttp3.logging.interceptor)//endregion//region rxjava2
// implementation(libs.androidx.runtime.rxjava2)
// implementation(libs.adapter.rxjava2)
// implementation(libs.rxjava)
// implementation(libs.rxandroid)//endregion//region rxjava3implementation(libs.rxjava3.rxjava)implementation(libs.adapter.rxjava3)implementation(libs.rxlifecycle)implementation(libs.rxlifecycle.android.lifecycle.kotlin)implementation(libs.rxlifecycle.components)implementation(libs.rxjava3.rxandroid)//endregion//region roomimplementation(libs.androidx.room.runtime)annotationProcessor(libs.androidx.room.compiler)kapt(libs.androidx.room.compiler)//endregion//region coil 异步网络图片加载implementation(libs.coil.compose)//endregion//region accompanistimplementation(libs.accompanist.permissions)//endregion//region 国际化implementation(libs.lingver)// endregion//region compose 约束布局implementation (libs.constraintlayout.compose)//endregion}
3.工程目录下的 build.gradle.kts
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {alias(libs.plugins.android.application) apply falsealias(libs.plugins.jetbrains.kotlin.android) apply falseid("com.google.dagger.hilt.android") version "2.51.1" apply falsekotlin("kapt") version "1.9.25" apply falsekotlin("jvm") version "1.9.25" apply false // or kotlin("multiplatform") or any other kotlin pluginkotlin("plugin.serialization") version "1.9.25" apply falseval room_version = "2.6.1"id("androidx.room") version room_version apply false
}
4.工程目录下setting.gradle.kts
pluginManagement {repositories {google {content {includeGroupByRegex("com\\.android.*")includeGroupByRegex("com\\.google.*")includeGroupByRegex("androidx.*")}}mavenCentral()gradlePluginPortal()}
}
dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {google()mavenCentral()maven("https://jitpack.io")}
}rootProject.name = "ComposeApp"
include(":app")
2.搭建Hilt环境
1.创建Application
@HiltAndroidApp
class MyApplication : Application() {companion object {const val SharedPreferencesFileName = "MyApplication";private lateinit var application: Application;lateinit var sharedPreferences: SharedPreferencesfun getApplication(): Application {return application;}}@Overrideoverride fun onCreate() {super.onCreate()
// Log.i("测试","app启动了 child="+child)application = thissharedPreferences =getSharedPreferences(SharedPreferencesFileName, Context.MODE_PRIVATE);/*** 国际化绑定 已经帮存入 sharedPreferences里了*/Lingver.init(this)}}
加上注解 @HiltAndroidApp
2.在用到注入的Activity上加上注解
@AndroidEntryPoint
class MainActivity : ComponentActivity() {/*** 将依赖注入好的viewModel放入这里*/private val viewModel: XianPageViewModel by viewModels()@Injectlateinit var child: Child;override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {val sharedPreferences = MyApplication.sharedPreferencesLog.i("share", "这是shared 全局 $sharedPreferences")// sharedPreferences.edit().putString("token", "123456").apply()//关闭 导航状态栏// 隐藏状态栏if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {window.insetsController?.hide(WindowInsets.Type.statusBars())} else {@Suppress("DEPRECATION")window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN}this.lifecycleScopeval string = sharedPreferences.getString("token", "")Log.i("share", "获取token 全局 $string")ComposeAppTheme {
// ScaffoldExample()
// MyApp()
// NamePageRxjava3()
// NamePage()
// RoomTestPage()
// NfcPage()
// NfcMain()
// ImageIconPage()
// PermissionPage()// LiveDataTest()
// LanguageSelector()LanguageSelectorKuangjia()}}// Log.i("测试","activity启动了 this="+this)
// Log.i("测试","activity启动了 child="+child.activity)
// Log.i("测试","activity启动了 child="+child.application)Log.i("测试", "activity启动了 viewModel=" + viewModel)}}
3.测试注入
/*** 第一种: 相当于spring @Component*/
@ActivityScoped
class Child @Inject constructor(@ActivityContext val activity: Context,@ApplicationContext val application: Context){val name = "你好"
// val applicationContext = application}
3.注入网络模块
1.单例类
/*** 全局唯一网络模块*/
@Module
@InstallIn(SingletonComponent::class)
object NetModel {private const val Tag = "Retrofit:";private const val URL = "http://192.168.202.57:8080";@Singleton@Providesfun provideOkHttpClient(tokenInterceptor: TokenInterceptor): OkHttpClient {//构建日志拦截器val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {override fun log(message: String) {Log.i(Tag, message)}}).setLevel(HttpLoggingInterceptor.Level.BODY)//构建return OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).addInterceptor(tokenInterceptor).addInterceptor(httpLoggingInterceptor).build()}@RxJava2Inject@Singleton@Providesfun provideRetrofitRxJava2(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(URL).client(okHttpClient)// .addConverterFactory(ScalarsConverterFactory.create())//添加Gson转换器.addConverterFactory(GsonConverterFactory.create())//添加Gson转换器//添加Rxjava适配
// .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()}@RxJava3Inject@Singleton@Providesfun provideRetrofitRxJava3(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(URL).client(okHttpClient).addCallAdapterFactory(RxJava3CallAdapterFactory.create())
// .addConverterFactory(ScalarsConverterFactory.create())//添加Gson转换器.addConverterFactory(GsonConverterFactory.create())//添加Gson转换器//添加Rxjava适配.build()}@Singleton@Providesfun provideMainTestService(@RxJava3Inject retrofit: Retrofit): MainTestService {return retrofit.create(MainTestService::class.java)}}
2.Token拦截器
@Singleton
class TokenInterceptor @Inject constructor() : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()val token: String = MyApplication.sharedPreferences.getString("token", "2001").toString()val url = request.url.newBuilder().addQueryParameter("token1", token).build()val header = request.headers.newBuilder().add("token2", token).build()val requestNew = request.newBuilder().url(url).headers(header).build()return chain.proceed(requestNew);}
}
3.要是注入两个相同类型的
创建自定义注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RxJava2Inject
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RxJava3Inject
在构造的时候声明
@RxJava2Inject@Singleton@Providesfun provideRetrofitRxJava2(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(URL).client(okHttpClient)// .addConverterFactory(ScalarsConverterFactory.create())//添加Gson转换器.addConverterFactory(GsonConverterFactory.create())//添加Gson转换器//添加Rxjava适配
// .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()}@RxJava3Inject@Singleton@Providesfun provideRetrofitRxJava3(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(URL).client(okHttpClient).addCallAdapterFactory(RxJava3CallAdapterFactory.create())
// .addConverterFactory(ScalarsConverterFactory.create())//添加Gson转换器.addConverterFactory(GsonConverterFactory.create())//添加Gson转换器//添加Rxjava适配.build()}
注入的时候声明注入哪个
@Singleton@Providesfun provideMainTestService(@RxJava3Inject retrofit: Retrofit): MainTestService {return retrofit.create(MainTestService::class.java)}
4.声明接口
interface MainTestService {// @GET("/phone/gis/test")
// suspend fun getTestData(): Response<String>@GET("/phone/gis/test")suspend fun getTestDataCall(): Call<TestDto>@GET("/phone/gis/test")suspend fun getTestData(): Response<TestDto>@GET("/phone/gis/test/{name}")suspend fun getTestDataTruth(@Path("name") name: String, @Query("age") age: Int): TestDto/*** 使用Rxjava 不允许加 suspend 关键字*/@GET("/phone/gis/test/{name}")fun getTestDataRx(@Path("name") name: String, @Query("age") age: Int): Observable<TestDto>}
RxJava写 接口,千万不要声明suspend ,要不然序列化JSON 会报错,无法实例化 Observable
5.要是发送http请求还需要权限
network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config><base-config cleartextTrafficPermitted="true" />
</network-security-config>
6.网络权限
<uses-permission android:name="android.permission.INTERNET"/>
4.ViewModel
/*** viewmodel不能设置作用域,否则会报错** ViewModel 部分中提到的 viewModel() 函数会自动使用 Hilt 通过 @HiltViewModel 注解构建的 ViewModel*/
@HiltViewModel
class ScopePageViewModel @Inject constructor():ViewModel() {init {val job: Job = viewModelScope.launch { }//可以单独取消}}
然后注入就可以了
5.LiveData
@HiltViewModel
class LiveDataTestViewModel @Inject constructor() : ViewModel() {val items:MutableLiveData<MutableList<Item>> = MutableLiveData(mutableListOf())init {val item1 = Item("测试模块1",true)items.value?.add(item1)val item2 = Item("测试模块2",true)items.value?.add(item2)}fun updateItem(item:Item){item.updateIsShow(!item.isShow.value!!)}}
用法
@Composable
fun LiveDataTest(viewModel: LiveDataTestViewModel = hiltViewModel()) {val items = viewModel.items.observeAsState(mutableListOf())LazyColumn(modifier = Modifier.fillMaxSize().padding(30.dp)) {item {Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {val width = Modifier.width(150.dp).height(50.dp)Button(onClick = {viewModel.updateItem(items.value[0])}, modifier = width) {Text(text = stringResource(R.string._1))}Button(onClick = { viewModel.updateItem(items.value[1]) }, modifier = width) {Text(text = "隐藏或打开测试模块2")}}}items(items.value) {ItemComponent(item = it)}}}
6.整合navigation
@Serializable
data class Profile(val name: String)@Serializable
object FriendsList@Serializable
object ProfileScreenChild2@Serializable
data class ProfileScreenChild1(val param: String)@Serializable
data class Aaa(val param: String)// Define the ProfileScreen composable.
@Composable
fun ProfileScreen(profile: Profile,parentViewModel: XianPageViewModel?,viewModel: XianPageViewModel = hiltViewModel(),onNavigateToFriendsList: () -> Unit) {// ProfileScreen启动了com.composeapp.ui.view.XianPageViewModel@841f9de这是viewmodelLog.i("测试", "ProfileScreen启动了" + viewModel.toString() + "这是viewmodel")Log.i("测试","ProfileScreen启动了 这是父级的viewModel实例" + parentViewModel?.toString() + "这是父类viewmodel")Column {Text("导航参数: ${profile.name}")ProfileScreenChild()Button(onClick = { onNavigateToFriendsList() }) {Text("Go to Friends List")}}
}@Composable
fun ProfileScreenChild(viewModel: XianPageViewModel = hiltViewModel()) {
// ProfileScreenChild启动了com.composeapp.ui.view.XianPageViewModel@841f9de这是viewmodelLog.i("测试", "ProfileScreenChild启动了" + viewModel.toString() + "这是viewmodel") //在同一个导航中是一致的
// Text("子组件的值: ${viewModel.data.value}")val childNavController = rememberNavController()NavHost(childNavController, startDestination = ProfileScreenChild1("你好测试子组件Pchild1")) {composable<ProfileScreenChild2> { backStackEntry ->//专门用来拿到当前导航的堆栈里的ViewModelval parentEntry = remember(backStackEntry) {//必须类型和值都一样才能找到,否则抛异常childNavController.getBackStackEntry(ProfileScreenChild1("你好测试子组件Pchild1"))//必须值要一样才能找到,值不一致就找不到,跟序列化JSON比}
// viewmodel 当组件移除组件树的时候,将会被销毁val parentViewModel = hiltViewModel<XianPageViewModel>(parentEntry)ProfileScreenChild2(parentViewModel)}composable<ProfileScreenChild1> { backStackEntry ->val child1 = backStackEntry.toRoute<ProfileScreenChild1>()ProfileScreenChild1(profileScreenChild1 = child1) {childNavController.navigate(ProfileScreenChild2)}}}}@Composable
fun ProfileScreenChild2(parentViewModel: XianPageViewModel,viewModel: XianPageViewModel = hiltViewModel()
) {Log.i("测试","ProfileScreenChild2启动了" + parentViewModel.toString() + "这是parent viewmodel") //在同一个导航中是一致的Log.i("测试","ProfileScreenChild2启动了" + viewModel.toString() + "这是viewmodel") //在同一个导航中是一致的Log.i("测试","ProfileScreenChild2启动了" + (viewModel === parentViewModel) + "这是viewmodel") //在同一个导航中是一致的Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {Button(onClick = { /*TODO*/ }) {Text(text = "ProfileScreenChild2")}}
}@Composable
fun ProfileScreenChild1(viewModel: XianPageViewModel = hiltViewModel(),profileScreenChild1: ProfileScreenChild1,toProChild2: () -> Unit
) {Log.i("测试","ProfileScreenChild1启动了" + viewModel.toString() + "这是viewmodel 这是param${profileScreenChild1.param}") //在同一个导航中是一致的Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {Button(onClick = { toProChild2() }) {Text(text = "ProfileScreenChild1")}}}// Define the FriendsListScreen composable.
@Composable
fun FriendsListScreen(onNavigateToProfile: (String) -> Unit,viewModel: XianPageViewModel = hiltViewModel()
) {Log.i("测试", "FriendsListScreen启动了" + viewModel.toString() + "这是viewmodel")val (text, setText) = remember { mutableStateOf("") }Column {Text("Friends List")TextField(value = text, onValueChange = setText)Button(onClick = { onNavigateToProfile(text) }) {Text("Go to Profile")}}
}// Define the MyApp composable, including the `NavController` and `NavHost`.
@Composable
fun MyApp(viewModel: XianPageViewModel = viewModel()) {Log.i("测试", "MyApp启动了 viewmodel=$viewModel")//是Activity同一个val navController = rememberNavController()NavHost(navController, startDestination = FriendsList) {composable<Profile> { backStackEntry ->
// 获取路由对象实例val profile: Profile = backStackEntry.toRoute()Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {ProfileScreen(profile = profile,parentViewModel = null,onNavigateToFriendsList = {navController.navigate(route = FriendsList)})}}composable<FriendsList> {Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {FriendsListScreen(onNavigateToProfile = { it ->navController.navigate(route = Profile(name = "传入了新的参数:${it}"))})}}}
}
7.整合room
1.实体类
@Entity
@Serializable
data class User(@PrimaryKey val uid:String,@ColumnInfo(name = "name")val name:String,val time:Long,val date:String?=null,val age :Int
) {
}
2.dao
@Dao
interface UserDao {@Query("SELECT * FROM user")fun getAll(): List<User>@Query("SELECT * FROM user WHERE uid = :userId")fun loadAllByIds(userId: String): List<User>@Query("SELECT * FROM user WHERE name LIKE :name")fun findByName(name: String): List<User>@Insertfun insertAll(vararg users: User)@Deletefun delete(user: User)}
3.数据库
@Database(entities = [User::class], version = 3, exportSchema = true)
abstract class AppDataBase: RoomDatabase() {companion object{private const val DB_NAME = "my_app.db"fun getInstance(context: Context): AppDataBase{val db = Room.databaseBuilder(context,AppDataBase::class.java, DB_NAME).addMigrations(MIGRATION_1_2,MIGRATION_2_3).build()return db;}val MIGRATION_1_2 = object : Migration(1, 2) {override fun migrate(db: SupportSQLiteDatabase) {// 执行 SQL 语句来迁移数据库db.execSQL("ALTER TABLE user ADD COLUMN date TEXT")}}val MIGRATION_2_3 = object : Migration(2, 3) {override fun migrate(db: SupportSQLiteDatabase) {// 执行 SQL 语句来迁移数据库db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL default 1")}}}/*** 数据库当中的一张表*/abstract fun UserDao():UserDao}
4.Dao实例注入
/*** 全局唯一数据库模块*/
@Module
@InstallIn(SingletonComponent::class)
object DataBaseModel {@Singleton@Providesfun provideAppDataBase(@ApplicationContext context:Context): AppDataBase {return AppDataBase.getInstance(context)}@Singleton@Providesfun provideUserDao(appDataBase: AppDataBase): UserDao {return appDataBase.UserDao()}}
8.Permission
@SuppressLint("CheckResult")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionPage(){// Camera permission stateval cameraPermissionState = rememberPermissionState(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)Observable.fromCallable {Log.i("执行","执行在"+Thread.currentThread().name)"true"}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe({Log.i("执行","执行成功$it "+Thread.currentThread().name)},{Log.i("执行","执行失败"+it.message)})Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){if (cameraPermissionState.status.isGranted) {Text("相机权限授予成功")} else {Column {val textToShow = if (cameraPermissionState.status.shouldShowRationale) {// If the user has denied the permission but the rationale can be shown,// then gently explain why the app requires this permission"相机权限是核心. Please grant the permission."} else {// If it's the first time the user lands on this feature, or the user// doesn't want to be asked again for this permission, explain that the// permission is required"必须给予相机权限. " +"Please grant the permission"}Text(textToShow)Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {Text("Request permission")}}}}}
9.Kotlin json 序列化
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json@Serializable
data class Project(val name: String, val language: String)fun main() {// Serializing objectsval data = Project("kotlinx.serialization", "Kotlin")val string = Json.encodeToString(data)println(string) // {"name":"kotlinx.serialization","language":"Kotlin"}// Deserializing back into objectsval obj = Json.decodeFromString<Project>(string)println(obj) // Project(name=kotlinx.serialization, language=Kotlin)
}
10.kotlin 冷流
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlockingfun main() {runBlocking {val coldFlow = flow {for (i in 1..5){delay(3000)emit(i)}}// 订阅者 1launch {coldFlow.collect { value ->println("Subscriber 1 received: $value")}}// 订阅者 2launch {delay(2000) // 延迟订阅,确保第二个订阅者在第一个订阅者之后coldFlow.collect { value ->println("Subscriber 2 received${Thread.currentThread().name}: $value")}}println("执行")}}
11.kotlin 热流
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlockingfun main() {runBlocking {// 创建一个 StateFlow 实例,初始状态为 0val stateFlow = MutableStateFlow(0)// 启动一个协程来更新 StateFlow 的状态launch {repeat(5) {delay(500) // 模拟数据生成stateFlow.value += 1}}// 订阅 StateFlow 并打印最新状态launch {stateFlow.collect { value ->println("Subscriber received: $value")}}
}}
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlockingfun main() {runBlocking {// 创建一个 SharedFlowval sharedFlow = MutableSharedFlow<Int>()// 启动一个协程来发射数据launch {repeat(10) {delay(100) // 模拟异步操作sharedFlow.emit(it) // 发射数据}}// 订阅 SharedFlowlaunch {sharedFlow.collect { value ->println("Subscriber 1 received: $value")}}// 另一个订阅者launch {sharedFlow.collect { value ->println("Subscriber 2 received: $value")}}}
}
12.全局语言切换
在 Application created 方法
/*** 国际化绑定 已经帮存入 sharedPreferences里了*/Lingver.init(this)
@Composable
fun LanguageSelectorKuangjia(localeViewmodel: LocaleViewmodel= hiltViewModel()) {// val locale = MyApplication.sharedPreferences.getString("locale",null)
//
// val localeState = localeViewmodel.locale.observeAsState(if (locale == null) Locale.getDefault() else Locale(locale)
// locale?.let { Locale(it) }?:Locale.getDefault()
// )val current = LocalContext.currentColumn(modifier = Modifier.fillMaxSize().padding(30.dp)) {// Example buttons to switch languagesRow {Button(onClick = {Lingver.getInstance().setLocale(current,Locale.SIMPLIFIED_CHINESE)(current as Activity).recreate()}) {Text("中文")}Button(onClick = {Lingver.getInstance().setLocale(current,Locale.ENGLISH)(current as Activity).recreate()}) {Text("英文")}}// Apply the selected language
// LanguageSwitcherKuangjia(localeState.value) {// Your UI content here, which will reflect the selected languageText(text = stringResource(id = R.string._1))
// }}}
自己写的扩展函数
/*** 转变语言扩展函数 会保留在当前的Navigation 导航里*/
fun Context.changeLocale(locale: Locale) {Lingver.getInstance().setLocale(this,locale)(this as Activity).recreate()
}
在 导航中 重创建,也会跟随导航目的地,不会重置
13.使用 阿里矢量图
iconfont-阿里巴巴矢量图标库
1.存一个地方
2.下载这个插件
3.右键一个空文件夹
4.操作
14.文件分享
1.文件提供者
<providerandroid:authorities="${applicationId}.fileprovider"android:name="androidx.core.content.FileProvider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths" /></provider>
2. 外部存储空间权限文件
file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths><external-path name="external_files" path="."/>
</paths>
3. MediaStore
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;public class ExportMedia {public static Uri saveExcelFileToMediaStoreInExternalDownloadExport(Context context, String fileName) {// 创建 ContentValues 对象ContentValues values = new ContentValues();values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);values.put(MediaStore.MediaColumns.MIME_TYPE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS+"/EXPORT"); // 下载目录// 插入文件到 MediaStorereturn context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);}public Uri queryExcelFiles(Context context, String fileName) {String[] projection = {MediaStore.Files.FileColumns._ID,MediaStore.Files.FileColumns.DISPLAY_NAME,MediaStore.Files.FileColumns.MIME_TYPE};String selection = MediaStore.Files.FileColumns.MIME_TYPE + "=?";String[] selectionArgs = {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}; // .xlsx 的 MIME 类型Cursor cursor = context.getContentResolver().query(MediaStore.Files.getContentUri("external"),projection,selection,selectionArgs,null);try {while (cursor.moveToNext()) {long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME));if (displayName.equals(fileName)){// 处理查询结果,例如分享文件return ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id);}}} catch (IllegalArgumentException e) {e.printStackTrace();}finally {if (cursor != null){cursor.close();}}return null;}
}
public class ShareUtils {public static void shareExcel(Context context,String path){// 文件路径,确保文件存在File fileToShare = new File(path);// 创建分享意图Intent shareIntent = new Intent(Intent.ACTION_SEND);shareIntent.setType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); // MIME 类型// 使用 FileProvider 获取 content URIUri fileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", fileToShare);shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);// 允许临时读取 URI 权限shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);// 启动分享对话框context.startActivity(Intent.createChooser(shareIntent, "分享一个Excel"));}public static void shareExcel(Context context,Uri uri){// 创建分享意图Intent shareIntent = new Intent(Intent.ACTION_SEND);shareIntent.setType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); // MIME 类型// 使用 FileProvider 获取 content URIshareIntent.putExtra(Intent.EXTRA_STREAM, uri);// 允许临时读取 URI 权限shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);// 启动分享对话框context.startActivity(Intent.createChooser(shareIntent, "分享一个Excel"));}
}
<!--在sdcard中创建/删除文件的权限 --><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" tools:ignore="ProtectedPermissions" /><uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" />
15. NFC
1.开启权限
<uses-permission android:name="android.permission.NFC" />
2.创建工具类
import android.nfc.tech.NfcA
import java.security.MessageDigestobject NfcHelper {private fun readUid(nfcA: NfcA): ByteArray {val command = byteArrayOf(xxxx.toByte(), xxxxx.toByte())val uid = nfcA.transceive(command)return byteArrayOf(uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], uid[7])}/*** 处理密码** @param nfcA* @return*/private fun handlePwd(nfcA: NfcA): ByteArray {val uidByte = readUid(nfcA).copyOf(7)val secret = byteArrayOf(xxxxxxxxxxxxxxxxxx)//两个数组重载运算符 拼接在一起val byteUnique = uidByte + secret//加密SHA -256val digest = MessageDigest.getInstance("SHA-256")// 更新 MessageDigest 实例,传入要哈希的数据digest.update(byteUnique)// 计算哈希值并返回 取前四位val pwdBytes = digest.digest().copyOf(4)return pwdBytes}/*** 新nfc芯片验证密码** @param nfcA* @return*/fun authNew(nfcA: NfcA) {val pwd = handlePwd(nfcA)//拼接两个数组val command = byteArrayOf(0x1B.toByte()) + pwdnfcA.transceive(command)}/*** 新nfc 芯片读方法** @param nfcA* @param startPage* @param endPage* @return*/fun readTag(nfcA: NfcA, startPage: Int, endPage: Int): List<Byte> {val list = arrayListOf<Byte>()for (i in startPage..endPage) {val data = nfcA.transceive(byteArrayOf(0x30, i.toByte()))list.addAll(data.asList().subList(0,4))}return list;}/*** 新nfc 芯片写方法** @param nfcA* @param writeByte* @param block 扇区,也就是页*/fun writeTag(nfcA: NfcA, writeByte: ByteArray, page: Int) {val cmd = byteArrayOf(0xA2.toByte(), page.toByte()) + writeBytenfcA.transceive(cmd)}}
3.Model
/*** 全局唯一NFC模块*/
@Module
@InstallIn(SingletonComponent::class)
object NfcModel {@Singleton@Providesfun provideNfcAdapter(@ApplicationContext context: Context): NfcAdapter {return NfcAdapter.getDefaultAdapter(context)}}
4.viewModel
import android.nfc.NfcAdapter
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject/*** viewmodel不能设置作用域,否则会报错*/
@HiltViewModel
class NfcViewModel @Inject constructor(val nfcAdapter: NfcAdapter):ViewModel() {// 使用 MutableLiveData 来持有数据}
5.页面
@Composable
fun NfcMain(){var open by remember { mutableStateOf(false) }Column(Modifier.fillMaxSize()) {Box(modifier = Modifier.fillMaxWidth().height(100.dp), contentAlignment = Alignment.Center){Button(onClick = { open = !open }) {Text(text = if (open) "点击关闭NFC" else "点击开启NFC")}}if (open){NfcPage()}else{Box(modifier = Modifier.fillMaxSize().background(Color.Blue), contentAlignment = Alignment.Center){Text(text = "NFC未开启")}}}}@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NfcPage(modifier: Modifier = Modifier, nfcViewModel: NfcViewModel = hiltViewModel()) {var presses by remember { mutableIntStateOf(0) }val readOrWrite = remember {mutableStateOf(false)}val (text, setText) = remember { mutableStateOf("") }val nfcAdapter = nfcViewModel.nfcAdapterval list = remember {mutableStateListOf<String>()}val context = LocalContext.currentLaunchedEffect(key1 = Unit) {val pendingIntent = PendingIntent.getActivity(context, 0, Intent(MyApplication.getApplication(), context::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_IMMUTABLE)val filters = arrayOf(IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED))val arrayOf = arrayOf(arrayOf<String>(NfcA::class.java.name))nfcAdapter.enableReaderMode(context as Activity?, { tag ->val nfcA = NfcA.get(tag)Log.i("NfcA", "NfcA: $nfcA")nfcA.timeout = 3000try {nfcA.connect()Log.i("测试","nfc超时时间 ${nfcA.timeout} ")Log.i("测试","nfc最大长度 ${nfcA.maxTransceiveLength} ")NfcHelper.authNew(nfcA)// NewNfcHelper.authNew(nfcA)if (readOrWrite.value){val readTag = NfcHelper.writeTag(nfcA, byteArrayOf(2,3,4,1), 4)Log.i("测试","写入成功 $readTag")}else{val readTag = NfcHelper.readTag(nfcA, 4, 4)Log.i("测试","读取成功 $readTag")}} catch (e: Exception) {e.printStackTrace()} finally {nfcA.close()}}, NfcAdapter.FLAG_READER_NFC_A, null);nfcAdapter.enableForegroundDispatch(context, pendingIntent, filters, arrayOf)}DisposableEffect(key1 = Unit) {this.onDispose {nfcAdapter.disableForegroundDispatch(context as Activity)nfcAdapter.disableReaderMode(context as Activity)}}Scaffold(topBar = {TopAppBar(colors = topAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer,titleContentColor = MaterialTheme.colorScheme.primary,),title = {Box(modifier = Modifier.fillMaxWidth(),contentAlignment = Alignment.Center // 设置 Box 的内容居中) {TextField(value = text, onValueChange = setText,modifier = Modifier.width(200.dp).height(50.dp),// 调整宽度// 调整高度textStyle = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp,lineHeight = TextUnit(200f, TextUnitType.Sp)), // 调整字体大小) // 调整内边距}})},bottomBar = {BottomAppBar(containerColor = MaterialTheme.colorScheme.primaryContainer,contentColor = MaterialTheme.colorScheme.primary,) {Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),horizontalArrangement = Arrangement.spacedBy(space = 12.dp),verticalAlignment = Alignment.CenterVertically) {Button(modifier = Modifier.weight(1f), onClick = {}) {Text(text = "添加")}Button(modifier = Modifier.weight(1f), onClick = {readOrWrite.value = !readOrWrite.value}) {Text(text = if (readOrWrite.value) "写入" else "读取")}Button(modifier = Modifier.weight(1f), onClick = {list.clear()}) {Text(text = "清空")}}}},floatingActionButton = {FloatingActionButton(onClick = { presses++ }) {Icon(Icons.Default.Build, contentDescription = "Add")}}) { innerPadding ->LazyColumn(modifier = Modifier.padding(innerPadding)) {items(items = list, key = { it }) {Text(text = it,modifier = Modifier.fillMaxWidth().height(40.dp),textAlign = TextAlign.Center)}}}
}