如何在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.rhkb.cn/news/231251.html

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

相关文章

JS 手写 new 函数

工作中我们经常会用到 new 关键字&#xff0c;new 一个构造函数生成一个实例对象&#xff0c;那么new的过程中发生了什么呢&#xff0c;我们今天梳理下 创建一个对象对象原型继承绑定函数this返回对象 先创建一个构造函数&#xff0c;原型上添加一个方法 let Foo function (n…

Python元组与字典的基础介绍

元组(tuple) 在Python中,元组是不可变的有序元素的序列 即创建后不可以被修改 创建方式val_name ([val],[val].....) #----------声明------------ tuple_1 (1,2,3) print(tuple_1)元组的运算 虽然说元组的额元素是不可以更改的,但元组之间可以使用,,*号进行运算,运算后会…

静态网页设计——宠物狗狗网(HTML+CSS+JavaScript)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 感谢大佬的视频&#xff1a; https://www.bilibili.com/video/BV1nk4y1X74M/?vd_source5f425e0074a7f92921f53ab87712357b 使用技术&#xff1a;HTMLCSSJS&#xff08;…

can/CANFD数据记录仪——冬标神器

冬测案例 新能源电池在冬标中要测试电池的电性能&#xff0c;热管理&#xff0c;充电&#xff0c;SOC的性能电动车的关键组之一是动力电池&#xff0c;动力电池的表现&#xff0c;除了依赖自身的材料&#xff0c;工艺等硬件素质外&#xff0c;还依赖电池管理系统的表现&#xf…

小型洗衣机哪个牌子质量好?五款内衣洗衣机便宜好用的牌子推荐

随着大家工作的压力越来越大&#xff0c;下了班之后只能想躺平&#xff0c;在洗完澡之后看着还需要手洗的内衣裤真的很头疼。有些小伙伴还有会攒几天再丢进去洗衣机里面一起&#xff0c;而且这样子是非常不好的&#xff0c;用过的内衣裤长时间不清洗容易滋生细菌&#xff0c;而…

Java设计模式-享元模式

目录 一、网站项目需求 二、传统方案 三、享元模式 &#xff08;一&#xff09;基本介绍 &#xff08;二&#xff09;原理类图 &#xff08;三&#xff09;内部状态和外部状态 &#xff08;四&#xff09;享元模式解决网站展现项目 &#xff08;五&#xff09;注意事项…

Linux系统安全

作为一种开放源代码的操作系统&#xff0c;linux服务器以其安全、高效和稳定的显著优势而得以广泛应用。 账号安全控制 用户账号是计算机使用者的身份凭证或标识&#xff0c;每个要访问系统资源的人&#xff0c;必须凭借其用户账号 才能进入计算机.在Linux系统中&#xff0c;提…

继电器光耦在微控制器中的应用

继电器是电子系统中的重要组件&#xff0c;用作使用低功率信号控制高功率电路的开关。继电器与微控制器的集成在各种应用中变得越来越普遍。该领域的一个重大进步是继电器光耦合器的使用&#xff0c;这是一种增强基于微控制器的系统的性能和可靠性的关键技术。 继电器光耦概述…

web3d-three.js场景设计器-TransformControls模型控制器

场景设计器-TransformControls 控制器 该控制器可以指定模型进入可控制模式-如图有三种控制方式 translate --移动模式 rotate -- 旋转模式 scale -- 缩放模式 方便布局过程中快捷对模型进行摆放操作。 引入方式 import { TransformControls } from three/examples/jsm/…

2024程序员应对挑战新方式竟然是……

2024年即将来临&#xff0c;无论2023是顺心还是不如意&#xff0c;一切都已经成为了过去式。无论在过去我们是陷入了一时的困窘&#xff0c;还是沉浸在繁花似锦的喜悦&#xff0c;我们都要保持头脑冷静&#xff0c;不被眼前迷障所困&#xff1b;我们任然要勇往直前&#xff0c;…

如何在Linux上部署1Panel面板并远程访问内网Web端管理界面

文章目录 前言1. Linux 安装1Panel2. 安装cpolar内网穿透3. 配置1Panel公网访问地址4. 公网远程访问1Panel管理界面5. 固定1Panel公网地址 前言 1Panel 是一个现代化、开源的 Linux 服务器运维管理面板。高效管理,通过 Web 端轻松管理 Linux 服务器&#xff0c;包括主机监控、…

看CHAT如何判断php Imagick writeImages写入gif已经完毕

CHAT回复&#xff1a;Imagick::writeImages() 是同步执行的&#xff0c;也就是说这个函数会阻塞直到 GIF 文件被完全写出。所以如果这个函数没有报错并成功返回&#xff0c;那么你可以认为 GIF 文件已经被完全写出了。 如果你想要在写出 GIF 文件后立即做一些操作&#xff08;例…

使用Gitea搭建自己的git远程仓库

Gitea 为什么需要自建仓库 原因只有一个&#xff1a;折腾。其实国内的码云加上github已经足够用了。 官方原话 Gitea 的首要目标是创建一个极易安装&#xff0c;运行非常快速&#xff0c;安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言&#xff0c;这使我们…

跟着cherno手搓游戏引擎【3】事件系统和预编译头文件

不多说了直接上代码&#xff0c;课程中的架构讲的比较宽泛&#xff0c;而且有些方法写完之后并未测试。所以先把代码写完。理解其原理&#xff0c;未来使用时候会再此完善此博客。 文件架构&#xff1a; Event.h:核心基类 #pragma once #include"../Core.h" #inclu…

大创项目推荐 深度学习卷积神经网络的花卉识别

文章目录 0 前言1 项目背景2 花卉识别的基本原理3 算法实现3.1 预处理3.2 特征提取和选择3.3 分类器设计和决策3.4 卷积神经网络基本原理 4 算法实现4.1 花卉图像数据4.2 模块组成 5 项目执行结果6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基…

肠道炎症与年龄和阿尔茨海默病病理学相关:一项人类队列研究

谷禾健康 ​阿尔茨海默 研究表明&#xff0c;慢性低水平的炎症&#xff08;“炎症衰老”&#xff09;可能是年龄相关疾病的一个介导因素&#xff0c;而肠道微生物通过破坏肠道屏障可能会促进炎症。 虽然老化和阿尔茨海默病&#xff08;AD&#xff09;与肠道微生物群组成的改变有…

服务网格 Service Mesh

什么是服务网格&#xff1f; 服务网格是一个软件层&#xff0c;用于处理应用程序中服务之间的所有通信。该层由容器化微服务组成。随着应用程序的扩展和微服务数量的增加&#xff0c;监控服务的性能变得越来越困难。为了管理服务之间的连接&#xff0c;服务网格提供了监控、记…

python练习3【题解///考点列出///错题改正】

一、单选题 1.【单选题】 ——可迭代对象 下列哪个选项是可迭代对象&#xff08; D&#xff09;&#xff1f; A.(1,2,3,4,5) B.[2,3,4,5,6] C.{a:3,b:5} D.以上全部 知识点补充——【可迭代对象】 可迭代对象&#xff08;iterable&#xff09;是指可以通过迭代&#xff…

数字逻辑电路入门:从晶体管到逻辑门

数字逻辑电路入门&#xff1a;从晶体管到逻辑门 这是数字逻辑电路中最基础的部分。但是并非那么容易理解。 1、晶体管 mosfet&#xff1a;场效应晶体管&#xff0c;是电压控制元件。cmos&#xff1a;是指由mos管构成的门级电路通常是互补的。BJT&#xff1a;一种三极管&…

dll文件是什么,如何解决dll文件丢失

在使用电脑时是否遇到过关于dll文件丢失的问题&#xff0c;遇到这样的问题你是否会不知所措&#xff0c;其实dll文件丢失的解决伴有很多&#xff0c;今天这篇文章就将和大家聊聊dll文件是什么&#xff0c;以及如何解决dll文件丢失的问题。 一.Dll文件的作用 代码重用和模块化…