如何在2024年编写Android应用程序

如何在2024年编写Android应用程序

本文将介绍以下内容:

  • 针对性能进行优化的单活动多屏幕应用程序 🤫(没有片段)。
  • 应用程序架构和模块化 → 每个层面。
  • Jetpack Compose 导航。
  • Firestore。
  • 应用程序架构(模块化特征驱动的清晰架构)。

为什么要在应用程序中遵循架构?

应用程序架构就像您代码的蓝图。它可以智能地组织事物,使您的应用程序具有以下优点:

  • 易于理解和修复:没有混乱的代码。您可以快速找到并编辑所需内容。例如,如果您想添加一个按钮(转到 UI 层)。如果您想更改 UI 组件的验证逻辑(转到用例层)。
  • 可扩展和具有未来性:添加新功能非常简单,并且在大多数情况下,应用程序可以轻松适应新技术。
  • 可测试和可靠:可以早期发现错误,从而使应用程序更加平稳、可靠。提示:尝试为核心层编写更多测试。
  • 有益于团队协作:开发人员可以同时处理不同部分,而不会相互干扰,特别是如果他们正在处理不同的功能。
  • 可持续和长久:您的应用程序将保持健康并保持相关性多年。

投资于架构,观察您的应用程序繁荣!

App Screens

该应用程序有2个屏幕:登录和注销。
Login Screen
Post-Login Screen

面向未来的灵活性

使用 Kotlin 构建的本机 Android 应用程序可以无缝地过渡到各种平台,包括 iOS、Android、macOS、Windows、Linux 等。这要归功于 Kotlin Multiplatform。

该应用程序模块化以实现其未来的灵活性。

模块化

以下是应用程序中的模块:

  • app
  • feature
  • core
  • utility
  • buildSrc

维护代码的分层设计


应用程序模块

职责
它就像我们应用程序的船长。它会将您带到应用程序的正确屏幕,适应不同的设备(手机、平板电脑、智能手表、电视等),甚至计划在不同的平台(Android、iOS 等)上进行冒险 —— 这都归功于 Kotlin 的魔法。

应用程序模块包含主活动(单个活动)和 Jetpack Compose 导航的 RootHost

Jetpack Compose 导航是什么?

在 Jetpack Compose 中,导航通常是使用 Navigation 组件管理的,就像在传统的基于 XML 的 Android UI 中一样。导航组件有助于在应用程序中的不同可组合(UI 组件)之间导航。
app module

应用程序包

应用程序包含 Koin 依赖注入框架的初始化代码和 Firebase 初始化代码。Firestore 是该应用程序的后端。在下面的文章中可以了解更多关于 Koin 的信息。

https://levelup.gitconnected.com/koin-the-kotlin-kmp-dependency-injection-framework-dafaf9b85639?source=post_page-----04c9d8614c27--------------------------------

package com.evicky.financeledger.applicationimport android.app.Application
import com.evicky.core.di.useCaseModule
import com.evicky.feature.di.viewModelModule
import org.koin.core.context.startKoinclass FinanceLedgerApplication: Application() {override fun onCreate() {super.onCreate()
//        FirebaseApp.initializeApp(this)startKoin {modules(dataSourceModule, repoModule, useCaseModule, viewModelModule, coroutineDispatcherProviderModule)}}
}

基于安全原因,Firestore 从 GitHub 存储库中删除。代码已被注释以供参考。

//MainActivity.kt
package com.evicky.financeledgerimport android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.evicky.financeledger.root.RootHost
import com.evicky.financeledger.ui.theme.FinanceLedgerThemeclass MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {FinanceLedgerTheme {RootHost()}}}
}
  • 你是否注意到了上面的代码,MainActivity 没有像 Android 的 View 系统(基于 XML 的旧视图系统)一样扩展 AppCompatActivity。它是扩展了 ComponentActivity,这意味着该代码使用了 Jetpack Compose 库来构建 UI。

  • onCreate 事件管理器具有 androidx.activity:activity-compose 库的 setContent lambda 函数。

  • setContent 是使用 Jetpack Compose 定义屏幕 UI 的入口点。它接受一个描述要显示的 UI 元素层次结构的 composable 函数作为参数。

  • FinanceLedgerThemeRootHost 是可组合函数。

  • setContent 使用了一个特定于此应用程序的 material 3 主题,名为 FinanceLedgerTheme

该主题包含了 RootHost 的可组合函数。

让我们看一下 RootHost 的实现。

Root package

package com.evicky.financeledger.rootimport androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.evicky.feature.login.signInScreen
import com.evicky.feature.register.navigateToRegisterScreen
import com.evicky.feature.register.registerScreen
import com.evicky.feature.util.SIGNIN_ROUTE
import com.evicky.utility.logger.Log@Composable
internal fun RootHost() {val rootController = rememberNavController()NavHost(navController = rootController,startDestination = SIGNIN_ROUTE,) {signInNavGraph(onSignInPress = {rootController.navigateToPostLoginScreen(it)})postLoginNavGraph()}}
  • @Composable 注解告诉编译器 RootHost 是一个可组合函数。

什么是注解?

在 Kotlin 中,注解是一种元数据形式,您可以将其添加到代码中,以提供有关代码元素(如类、函数或属性)的额外信息。Kotlin 中的注解类似于 Java 中的注解。

在这里,@Composable 将函数标记为 UI 组件。这些函数被称为可组合函数,它们定义了用户界面的特定部分,并且可以嵌套使用以创建 UI 元素的层次结构。

RootHost() 函数包含了组合导航代码。它将引导您到应用程序中的正确屏幕,成为您应用程序的导航器!

在 Jetpack Compose 中,通常使用导航组件来管理导航,就像在传统的基于 XML 的 Android UI 中一样。导航组件有助于在应用程序中的不同可组合(UI 组件)之间进行导航。

让我们稍后继续讨论代码!

Utility module

职责:

实用程序或助手模块的目的是为所有模块提供应用程序中常见或可重复使用的功能。

utility module

Logger package

为什么我们需要有一个日志记录器包,当我们在 Android 中有默认的 Logging 库时呢?

一些第三方开源日志记录库具有很好的功能。有时您的应用程序可能有多个日志记录库。如果我们以这种方式拥有它,那么日志记录库将很容易地添加/替换。无需更改应用程序的每个文件中都有日志的文件。为了更容易更换日志记录库。

为什么要更换日志记录库?

回到2021年,您可能听说过著名的开源日志记录库“Log4j”中报告了一个关键漏洞。全世界的开发人员都因听到这个漏洞而感到困惑,并且在短时间内难以更换他们的日志记录库。

Log4j 漏洞

  • 发现:在广泛用于 Java 应用程序中的受欢迎的 Log4j 日志记录库中发现了一个关键漏洞(CVE-2021-44228)。
  • 影响:它允许攻击者在受影响的系统上执行任意代码,可能导致数据盗窃、勒索软件攻击或完全接管系统。
  • 严重性:由于易于利用和广泛使用,被评为“关键”。
package com.evicky.utility.loggerimport android.util.Logobject Log {fun v(tag: String, msg: String?, throwable: Throwable? = null) =msg?.run {Log.v(tag, this, throwable)}fun d(tag: String, msg: String?, throwable: Throwable? = null) =msg?.run {Log.d(tag, this, throwable)}fun i(tag: String, msg: String?, throwable: Throwable? = null) =msg?.run {Log.i(tag, this, throwable)}fun w(tag: String, msg: String?, throwable: Throwable? = null) =msg?.run {Log.w(tag, this, throwable)}fun e(tag: String, msg: String?, throwable: Throwable? = null) =msg?.run {Log.e(tag, this, throwable)}}

Network package

网络包包含用于检查互联网连接性的代码。它会 ping Google 的轻量级服务器以了解网络连接状态。在应用程序中了解互联网连接性至关重要。

请参考 GitHub 存储库获取代码。

Utils package

此包包含帮助程序类。

为什么我们应该在 helper 类中有 Coroutine Dispatcher Provider

协程中的硬编码分发器使协程的可测试性更加困难。如果您在 Android Studio 中集成了它,Sonar Lint 将报告警告。因此,协程的分发器从此辅助类中注入。

请参考下面的文章系列,以了解有关 Kotlin 协程的更多信息。

https://levelup.gitconnected.com/the-ultimate-kotlin-coroutine-concept-test-on-android-part-1-826d8066d0c4?source=post_page-----04c9d8614c27--------------------------------

Core module

职责:
核心模块包括用例、存储库、数据模型和数据源。您甚至可以有一个以上的模块,每个模块有两个层级。决策应根据您的需求进行。
Use cases(用例)→ 封装业务逻辑并协调交互。
Repositories(存储库)→ 充当数据访问的抽象层,决定使用哪个数据源。
Data models(数据模型)→ 表示应用程序数据的结构。
Data sources(数据源)→ 处理与底层存储的直接交互,管理读写操作。
这种分层方法促进了Android应用程序中的模块化、可扩展性,并且便于进行测试。即使您使用iOS或Android,此核心模块也不会被大幅修改。它独立于设备形态、操作系统和本地化。

core module

DataSource package

DataSource → 处理与底层存储的直接交互,管理读写操作。

package com.evicky.core.dataSourceimport com.evicky.utility.logger.Log
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeinterface IDataSource {suspend fun writeData(documentPath: String,data: Any,logTag: String,writeSuccessLogMessage: String,writeFailureLogMessage: String): Booleansuspend fun readData(logTag: String, documentPath: String): Map<String, Any>?suspend fun deleteData(documentPath: String,logTag: String,writeFailureLogMessage: String): Boolean}class Firestore : IDataSource {
//    private val fireStore: FirebaseFirestore = FirebaseFirestore.getInstance()override suspend fun writeData(documentPath: String,data: Any,logTag: String,writeSuccessLogMessage: String,writeFailureLogMessage: String,) = suspendCancellableCoroutine { continuation ->try {
//            fireStore.document(documentPath).set(data)
//                .addOnCompleteListener { task ->
//                    if (task.isSuccessful) {
//                        Log.i(logTag, "Data write success to firestore db $writeSuccessLogMessage: $documentPath")
//                        if (continuation.isActive) continuation.resume(true)
//                    } else {
//                        Log.e(logTag,
//                            "Data write failed to firestore db $writeFailureLogMessage: $documentPath. Error Message: ${task.exception?.message}",
//                            task.exception)
//                        if (continuation.isActive) continuation.resume(false)
//                    }
//                }
//            sendSuccessResultIfOfflineForDocumentWrite(fireStore.document(documentPath), continuation)} catch (exception: Exception) {Log.e(logTag,"Exception from execution: Data write failed to firestore db. $writeFailureLogMessage: $documentPath. Error Message: ${exception.message}",exception)if (continuation.isActive) continuation.resume(false)}}override suspend fun readData(logTag: String, documentPath: String): Map<String, Any>? =
//        fireStore.document(documentPath).get().await().datanulloverride suspend fun deleteData(documentPath: String,logTag: String,writeFailureLogMessage: String,) = suspendCancellableCoroutine { continuation ->try {
//            fireStore.document(documentPath).delete()
//                .addOnCompleteListener { task ->
//                    if (task.isSuccessful) {
//                        Log.i(logTag, "Delete Success in firestore db: $documentPath")
//                        if (continuation.isActive) continuation.resume(true)
//                    } else {
//                        Log.e(logTag,
//                            "Delete Failure in firestore db: $writeFailureLogMessage: $documentPath. Error Message: ${task.exception?.message}",
//                            task.exception)
//                        if (continuation.isActive) continuation.resume(false)
//                    }
//
//                }} catch (exception: Exception) {Log.e(logTag,"Exception from execution: Delete Failure in firestore db: $writeFailureLogMessage: $documentPath. Error Message: ${exception.message}",exception)if (continuation.isActive) continuation.resume(false)}}
}/*** While the device is in offline we won't get success or failure task result for writes to firestore.* So getting the meta data to send the result back to the caller if the data is from cache.*/
//fun sendSuccessResultIfOfflineForDocumentWrite(
//    documentReference: DocumentReference,
//    continuation: CancellableContinuation<Boolean>
//) {
//    documentReference.get().addOnSuccessListener {
//        if (it.metadata.isFromCache && continuation.isActive) continuation.resume(true)
//    }
//}class DynamoDb : IDataSource {override suspend fun writeData(documentPath: String,data: Any,logTag: String,writeSuccessLogMessage: String,writeFailureLogMessage: String,): Boolean {Log.i(logTag, "Data write success to dynamo db $writeSuccessLogMessage: $documentPath")return true}override suspend fun readData(logTag: String, documentPath: String): Map<String, Any>? {Log.i(logTag, "Data read success from dynamo db $documentPath")return null}override suspend fun deleteData(documentPath: String,logTag: String,writeFailureLogMessage: String,): Boolean {Log.i(logTag, "Data delete success in dynamo db $documentPath")return true}
}

为什么我们应该为 dataSource 有一个接口和类?难道只使用类就不够了吗?因为 repository 将为测试模拟拥有接口和类。

repositorydataSource 是否应该有专门的接口和类?

虽然在存储库中只有一个类对数据源有一个接口也是可行的,但是在 Android Clean Architecture 中为存储库和数据源引入接口提供以下优点:

1)可测试性:

对于存储库:拥有存储库接口使得更容易创建模拟实现以测试更高级别的组件。
对于数据源:拥有数据源接口允许更容易地测试依赖于数据源的组件,因为您可以提供模拟实现。

2)多个实现:

对于存储库:存储库接口允许多个存储库实现(例如远程和本地),轻松切换或合并数据源而不影响更高级别的组件(例如 usecaseviewmodels 等)。
对于数据源:数据源接口允许多个数据源实现(例如 API 和本地数据库),提供灵活性。

3)易于重构:

对于存储库:如果底层数据源实现发生更改,拥有存储库接口可以使您在不影响更高级别的组件的情况下进行更改。
对于数据源:如果数据源实现发生更改,则数据源接口提供了明确的合同,使得更容易更新实现。
在此应用程序中有两个数据源。Google 的 Firestore 和 Amazon 的 DynamoDB。根据用户类型,数据源会动态地绑定到应用程序。用户类型决策是在存储库层中进行的。稍后将讨论这个问题。

出于安全原因,Firestore 已从 GitHub 存储库中删除。DynamoDb 实现没有实际代码,如果需要,您可以实现它。

我相信以上代码对开发人员很友好。如果不是,请告诉我。我会尝试解释一下。

Repo package

Repositories → 充当数据访问的抽象层,决定使用哪个数据源。

package com.evicky.core.repoimport com.evicky.core.dataSource.DynamoDb
import com.evicky.core.dataSource.Firestore
import com.evicky.core.dataSource.IDataSource
import com.evicky.core.model.local.SignInLocalDatainterface ISignInRepo {suspend fun writeData(documentPath: String, data: SignInLocalData, logTag: String): Booleansuspend fun readData(logTag: String, path: String): Map<String, Any>
}class SignInRepo(firestore: Firestore, dynamoDb: DynamoDb): ISignInRepo {private val isPremiumUser = false // This data may come from some other data sourceprivate val dataSource: IDataSource = if (isPremiumUser) dynamoDb else firestoreoverride suspend fun writeData(documentPath: String, data: SignInLocalData, logTag: String): Boolean = dataSource.writeData(documentPath = documentPath, data =  data, logTag = logTag, writeSuccessLogMessage = "Data written successfully", writeFailureLogMessage = "Data write failed")override suspend fun readData(logTag: String, path: String): Map<String, Any> = dataSource.readData(logTag, documentPath = path) ?: mapOf()}

在数据源包部分,为什么同时需要接口和类的原因已经说明。在这里,接口用于关注点分离和测试。

数据源的选择是由存储库层决定的。在这里,数据源是通过 isPremiumUser 布尔状态的帮助来选择的。这些数据来自其他数据源。它可以来自 jetpack datastore,或者像 mongoDB 或 Firestore 或 DynamoDB 这样的单独数据库,或者来自应用内购买信息。在这里,为了简单起见,它是硬编码的。

数据源是单例,由 koin di 框架注入。

Firestore 在此处被选为数据源。

Model package

Data models → 表示应用程序数据的结构。

package com.evicky.core.model.localdata class SignInLocalData(val id: Long, val name: String)
package com.evicky.core.model.remoteimport android.os.Parcelable
import kotlinx.parcelize.Parcelize@Parcelize
data class SignInRemoteData(val id: Long, val name: String, val address: String, val occupat

在登录过程中有两个数据模型。

SignInRemoteData 是从数据源获取的,它包含了已登录用户的所有详细信息。

SignInLocalData 是从 SignInRemoteData 派生而来的。因为用户界面不需要已登录用户的每个细节,所以在更高级别的组件中(如用例、视图模型、组合等)使用 SignInLocalData。

Usecase package

Use cases → 封装业务逻辑并协调交互。

package com.evicky.core.usecaseimport com.evicky.core.model.local.SignInLocalData
import com.evicky.core.model.remote.SignInRemoteData
import com.evicky.core.repo.ISignInRepo
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Jsonclass SignInUseCase(private val signInRepo: ISignInRepo) {suspend fun writeData(data: SignInLocalData, logTag: String): Boolean {return signInRepo.writeData(documentPath = "Users/UserId", data = data, logTag = logTag)}suspend fun readData(path: String, logTag: String): SignInLocalData {val signInRemoteData = Json.decodeFromString<SignInRemoteData>(signInRepo.readData(logTag = logTag, path = path).toString())return SignInLocalData(id = signInRemoteData.id, name = signInRemoteData.name)}}

存储库是一个工厂(每个注入将给出一个新实例),由 koin di 框架注入。

用例具有纯业务逻辑。它将数据源的 SignInRemoteData 转换为更高级别组件的 SignInLocalData

这里使用 Kotlinx-serialization 库进行 Json 数据解析。

https://github.com/Kotlin/kotlinx.serialization

什么是业务逻辑?

业务逻辑是指特定的规则、计算和操作,定义了应用程序如何处理和操作数据以实现业务目标。在软件开发的背景下,业务逻辑通常封装在应用程序的后端中,远离用户界面,并负责实现系统的核心功能。

在清洁架构或类似的架构模式中,业务逻辑通常被组织成用例。用例表示系统向其用户提供的特定、离散的功能。用例封装了执行特定操作或实现应用程序中特定目标所需的逻辑。

Feature module

职责:
feature module充当表示层。它负责向用户呈现用户界面。
它包含了每个应用程序特性的视图模型(状态持有者(state holder))、Compose 导航图和组合(用户界面)(compose navgraph and the composable (UI) )。

feature module
SignIn Screen
包含上面Screen的ui 和 state holder代码。

什么是状态持有者?

“state holder”通常指的是一种机制,用于在配置更改(如屏幕旋转)或 Android 组件(如 Activity 或 Fragment)的生命周期中管理和保留与 UI 相关的数据。

ViewModel 旨在以适应生命周期的方式持有和管理与 UI 相关的数据,使数据能够在配置更改时保留。这是通过将 ViewModel 与特定的生命周期关联起来实现的,通常与关联的 UI 组件的生命周期相关联。

这并不意味着只有 viewModel 才能作为状态持有者。一个普通的类也可以充当状态持有者。请参考下面的链接。

https://developer.android.com/jetpack/compose/state-hoisting?#classes-as-state-owner
https://developer.android.com/jetpack/compose/state-hoisting?#viewmodels-as-state-owner

SignInGraph:

是否需要为多个屏幕创建多个活动或片段?

不需要。在引入Jetpack Compose及其导航组件后,这个说法已经过时。

使用Jetpack Compose进行导航

导航组件为Jetpack Compose应用程序提供支持。您可以在不同的组合项之间进行导航。

在此应用程序中,使用了Compose导航进行屏幕导航。

以下是Jetpack Compose导航的工作原理概述:

  • NavHost: NavHost作为可导航到的屏幕(组合项)的容器。
  • NavGraph: NavHost包含定义导航路由的NavGraph。它指定了应用程序中不同组合项之间的连接。
  • Navigation Actions: 通过调用导航操作来触发导航。例如,您可以使用navigate函数从一个组合项跳转到另一个组合项。
  • Back Navigation: Jetpack Compose还提供了使用navController的popBackStack()函数处理返回导航的方法。

现在您已经了解了导航的工作原理。让我们进入代码。

package com.evicky.feature.signInimport androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.evicky.feature.util.SIGNIN_ROUTE
import org.koin.androidx.compose.koinViewModelfun NavGraphBuilder.signInNavGraph(onSignInPress: (String) -> Unit
) {composable(route = SIGNIN_ROUTE) {val viewModel: SignInViewModel =  koinViewModel()val loginUiState by viewModel.signInUiState.collectAsStateWithLifecycle()SignInScreen(signInUiState = loginUiState,onSignInPress = { if (loginUiState.errorMessage.isEmpty() && loginUiState.phoneNumber.isNotEmpty()) onSignInPress.invoke(it) },onPhoneNumberChange = { updatedValue ->viewModel.onPhoneNumberChange(updatedValue)})}
}

这个NavGraphBuilder扩展函数的调用者位于App模块的Root包中。

MainActivity中的RootHost()函数调用了NavHostNavHost调用了这个SignInNavGraph

为了简洁起见,我在这里再次粘贴代码:

@Composable
internal fun RootHost() {val rootController = rememberNavController()NavHost(navController = rootController,startDestination = SIGNIN_ROUTE,) {signInNavGraph(onSignInPress = {rootController.navigateToPostLoginScreen(it)})postLoginNavGraph()}}
  • NavHost应该只有一个NavController。这是通过上面的rememberNavController()函数创建的。
  • NavHost具有控制器和起始目标。起始目标是一个必需的参数,如果没有它,代码将无法运行。
  • 起始目标的NavGraph应该在NavHost的lambda表达式中给出,否则代码将无法运行。
  • SIGNIN_ROUTE是一个字符串,它充当路由字符串,就像Web应用程序的URL一样。例如,app://com.evicky.finance/jetpack。在这里,"jetpack"是特定组合函数的路由,调用导航操作时将呈现屏幕。
  • NavGraph应该至少有一个组合函数,并且该函数应该具有路由。路由应该与NavHost的起始目标相同。否则,应用程序将无法运行。
  • 在这里,NavHost有两个相当于两个屏幕的导航图。
  • NavGraphBuilder.signInNavGraph()有一个名为SignInScreen的屏幕。
  • SignInScreen包含渲染UI的代码。
  • SignInScreen由嵌套的组合函数组成,这些函数将为用户渲染UI。

本文不讨论SignInScreen的代码,因为它超出了本文的范围。

未来将会为Jetpack Compose制作专门的文章系列。请继续关注作者的最新动态。

BuildSrc模块

该模块包含所有模块的Gradle文件的代码。该模块利用TOML文件和Gradle约定插件的功能,提供简洁且可重用的Gradle代码。

查看更多内容:

https://levelup.gitconnected.com/say-goodbye-to-script-duplication-gradle-convention-plugin-for-android-code-reuse-multi-module-21c0eb24447d?source=post_page-----04c9d8614c27--------------------------------

Github

https://github.com/evignesh/FinLedgerPublic

参考

https://developer.android.com/topic/modularization
https://developer.android.com/topic/architecture/intro
https://developer.android.com/jetpack/compose/documentation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/590653.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】STL 容器 - map 关联容器 ③ ( map 容器常用 api 操作 | map 容器迭代器遍历 | map#insert 函数返回值处理 )

文章目录 一、map 容器迭代器遍历1、map 容器迭代器2、代码示例 二、map 容器插入结果处理1、map#insert 函数返回值处理2、代码示例 一、map 容器迭代器遍历 1、map 容器迭代器 C 语言中 标准模板库 ( STL ) 的 std::map 容器 提供了 begin() 成员函数 和 end() 成员函数 , 这…

Seata服务搭建与模式实现

日升时奋斗&#xff0c;日落时自省 目录 1、简述 2、Seata优越性 3、Seata组成 4、Seata模式 4.1、XA 模式 4.2、AT 模式(默认模式) 4.3、TCC 模式 4.4、SAGA 模式 4.5、XA协议 5、Seata服务部署 5.1、文件数据源部署 5.1.1、下载并安装Seata 5.1.2、启动Seata服…

Ts自封装WebSocket心跳重连

WebSocket是一种在单个TCP连接上进行全双工通信的协议&#xff0c;允许客户端和服务器之间进行双向实时通信。 所谓心跳机制&#xff0c;就是在长时间不使用WebSocket连接的情况下&#xff0c;通过服务器与客户端之间按照一定时间间隔进行少量数据的通信来达到确认连接稳定的手…

HarmonyOS4.0系统性深入开发11通过message事件刷新卡片内容

通过message事件刷新卡片内容 在卡片页面中可以通过postCardAction接口触发message事件拉起FormExtensionAbility&#xff0c;然后由FormExtensionAbility刷新卡片内容&#xff0c;下面是这种刷新方式的简单示例。 在卡片页面通过注册Button的onClick点击事件回调&#xff0c;…

数据库中的时间和前台展示的时间不一样,如何保存日期格式的数据到数据库? 如何展示数据库的日期数据到前台

我 | 在这里 &#x1f575;️ 读书 | 长沙 ⭐软件工程 ⭐ 本科 &#x1f3e0; 工作 | 广州 ⭐ Java 全栈开发&#xff08;软件工程师&#xff09; ✈️公众号 | 热爱技术的小郑 文章底部有个人公众号二维码。回复 Java全套视频教程 或 前端全套视频教程 即可获取 300G 教程资料…

【深入浅出RocketMQ原理及实战】「云原生升级系列」打造新一代云原生“消息、事件、流“统一消息引擎的融合处理平台

打造新一代云原生"消息、事件、流"统一消息引擎的融合处理平台 云原生架构RocketMQ的云原生架构实现RocketMQ的云原生发展历程互联网时期的诞生无法支持云原生的能力 云原生阶段的升级云原生升级方向促进了Mesh以及多语言化发展可分合化的存算分离架构存储分离架构的…

听GPT 讲Rust源代码--library/portable-simd

File: rust/library/portable-simd/crates/core_simd/examples/spectral_norm.rs spectral_norm.rs是一个示例程序&#xff0c;它展示了如何使用Portable SIMD库中的SIMD&#xff08;Single Instruction Multiple Data&#xff09;功能来实现频谱规范化算法。该示例程序是Rust源…

跟着cherno手搓游戏引擎【2】:日志系统spdlog和premake的使用

配置&#xff1a; 日志库文件github&#xff1a; GitHub - gabime/spdlog: Fast C logging library. 新建vendor文件夹 将下载好的spdlog放入 配置YOTOEngine的附加包含目录&#xff1a; 配置Sandbox的附加包含目录&#xff1a; 包装spdlog&#xff1a; 在YOTO文件夹下创建…

在Django中配置PostgreSQL

下载并安装PostgreSQL PostgreSQL: Downloads 安装依赖psycopg2 python -m pip install psycopg2 修改Django配置文件settings.py &#x1f4cc;编辑 mysite/settings.py 文件前&#xff0c;先设置 TIME_ZONE 为你自己时区。 LANGUAGE_CODE zh-Hans TIME_ZONE Asia/Shang…

【Elasticsearch源码】 分片恢复分析

带着疑问学源码&#xff0c;第七篇&#xff1a;Elasticsearch 分片恢复分析 代码分析基于&#xff1a;https://github.com/jiankunking/elasticsearch Elasticsearch 8.0.0-SNAPSHOT 目的 在看源码之前先梳理一下&#xff0c;自己对于分片恢复的疑问点&#xff1a; 网上对于E…

【基础】【Python网络爬虫】【12.App抓包】reqable 安装与配置(附大量案例代码)(建议收藏)

Python网络爬虫基础 App抓包1. App爬虫原理2. reqable 的安装与配置reqable 安装教程reqable 的配置 3. 模拟器的安装与配置夜神模拟器的安装夜神模拟器的配置配置代理配置证书 4. 内联调试及注意事项软件启动顺开启抓包功reqable面板功列表部件功能列表数据快捷操作栏 夜神模拟…

WPF+Halcon 培训项目实战 完结(13):HS 鼠标绘制图形

文章目录 前言相关链接项目专栏运行环境匹配图片矩形鼠标绘制Halcon添加右键事件Task封装运行结果个人引用问题原因推测 圆形鼠标绘制代码运行结果 课程完结&#xff1a; 前言 为了更好地去学习WPFHalcon&#xff0c;我决定去报个班学一下。原因无非是想换个工作。相关的教学视…

【java爬虫】股票数据获取工具前后端代码

前面我们有好多文章都是在介绍股票数据获取工具&#xff0c;这是一个前后端分离项目 后端技术栈&#xff1a;springboot&#xff0c;sqlite&#xff0c;jdbcTemplate&#xff0c;okhttp 前端技术栈&#xff1a;vue&#xff0c;element-plus&#xff0c;echarts&#xff0c;ax…

K8s实战入门

1.NameSpace Namespace是kubernetes系统中的一种非常重要资源&#xff0c;它的主要作用是用来实现多套环境的资源隔离或者多租户的资源隔离。 默认情况下&#xff0c;kubernetes集群中的所有的Pod都是可以相互访问的。但是在实际中&#xff0c;可能不想让两个Pod之间进行互相…

在线智能防雷监控检测系统应用方案

在线智能防雷监控检测系统是一种利用现代信息技术&#xff0c;对防雷设施的运行状态进行实时监测、管理和控制的系统&#xff0c;它可以有效提高防雷保护的安全性、可靠性和智能化程度&#xff0c;降低运维成本和风险&#xff0c;为用户提供全方位的防雷解决方案。 地凯科技在…

排序算法之计数排序

计数排序是一种非基于比较的排序算法&#xff0c;它通过统计数组中每个元素出现的次数&#xff0c;将其按次数从小到大排序。 以下是计数排序的基本步骤&#xff1a; 统计&#xff1a;统计数组中每个元素出现的次数。计数&#xff1a;将每个元素的出现次数存储在另一个数组中…

redisson作为分布式锁的底层实现

1. redisson如何实现尝试获取锁的逻辑 如何实现在一段的时间内不断的尝试获取锁 其实就是搞了个while循环&#xff0c;不断的去尝试获取锁资源。但是因为latch的存在会在给定的时间内处于休眠状态。这个事件&#xff0c;监听的是解锁动作&#xff0c;如果解锁动作发生。会调用…

Android textview展示富文本内容

今天实现的内容&#xff0c;就是上图的效果&#xff0c;通过Span方式展示图片&#xff0c;需要支持文字颜色改变、加粗。支持style\"color:green; font-weight:bold;\"展示。尤其style标签中的font-size、font-weight是在原生中不被支持的。 所以我们今天需要使用自…

病情聊天机器人,利用Neo4j图数据库和Elasticsearch全文搜索引擎相结合

项目设计目的&#xff1a; 本项目旨在开发一个病情聊天机器人&#xff0c;利用Neo4j图数据库和Elasticsearch全文搜索引擎相结合&#xff0c;实现对病情相关数据的存储、查询和自动回答。通过与用户的交互&#xff0c;机器人可以根据用户提供的症状描述&#xff0c;给出初步的可…

字母简化(UPC练习)

题目描述 给出一串全部为小写英文字母的字符串&#xff0c;要求把这串字母简化。简化规则是&#xff1a;统计连续出现的字母数&#xff0c;输出时先输出个数&#xff0c;再输出字母。比如&#xff1a;aaabbbaa&#xff0c;则简化为3a3b2a&#xff1b;而zzzzeeeeea&#xff0c;…