在Android应用中使用Clean架构

超越传统—Clean架构打造现代Android架构指南 Clean 架构是一种现代的软件架构设计原则,旨在解决传统 Android 架构的问题,提高代码的可维护性、可测试性和可扩展性。它的核心思想是通过解耦合和依赖反转来实现高内聚低耦合的代码结构。 阅读详情

自从开始开发安卓应用,我一直感觉我可以做得更好。我看过不少烂代码,其中当然有我写的。安卓系统的复杂性加上烂代码势必酿成灾祸,所以从错误中成长就很重要。我Google了如何更好地开发应用,发现了这个叫做Clean架构的东西。于是我尝试将它应用于安卓开发,根据我在类似项目中的经验做了一些改善,写出了这篇我觉得较为实用、值得分享的文章。

我会在这篇文章中手把手教你在Android应用中使用Clean架构。我最近一直用这种方式优雅地编写应用。

什么是Clean架构?

有许多文章已经很好地回答了这个问题。我在这里讲一讲Clean架构的核心概念。

一般来说,在Clean架构中,代码被分层成洋葱形,层层包裹,其中有一个依赖性规则:内层不能依赖外层,即内层不知道有关外层的任何事情,所以这个架构是向内依赖的。看个图感受一下:
clean architecture
图片由Bob大叔提供

Clean架构可以使你的代码有如下特性:

  1. 独立于架构
  2. 易于测试
  3. 独立于UI
  4. 独立于数据库
  5. 独立于任何外部类库

我将通过下面的例子解释这些特性是怎么来的。如果你想深入了解Clean架构,不妨看这篇文章和这个视频

Clean在Android中如何表现

一般来说,一个应用可以有任意数目的层,但除非你的应用到处是企业级功能逻辑,一般需要这三层:

  • 外层:实现层
  • 中层:接口适配层
  • 内层:逻辑层

接口实现层是体现架构细节的地方。实现架构的代码是所有不用来解决问题的代码,这包括所有与安卓相关的东西,比如创建Activity和Fragment,发送Intent以及其他联网与数据库的架构相关的代码。

添加接口适配层的目的就是桥接逻辑层和架构层的代码。

最重要的是逻辑层,这里包含了真正解决问题的代码。这一层不包含任何实现架构的代码,不用模拟器也应能运行这里的代码。这样一来你的逻辑代码就有了易于测试、开发和维护的优点。这就是Clean架构的一个主要的好处。

每一个位于核心层外部的层都应能将外部模型转成可以被内层处理的内部模型。内层不能持有属于外层的模型类的引用。这也是由于刚才说的依赖性规则,这样内外层可以很好地分离。

为什么要进行模型转换呢?举个例子,当逻辑层的模型不能直接很优雅地展现给用户,或是需要同时展示多个逻辑层的模型时,最好创建一个ViewModel类来更好的进行UI展示。这样一来,你就需要一个属于外层的Converter类来将逻辑层模型转换成合适的ViewModel。

再举一个例子:你从外部数据库层获得了ContentProvider的Cursor对象,外层首先要将这个对象转换成内层模型,再将它传给内层处理。

在文章的最后我还提供了一些学习资源。我们已经知道了Clean架构的基本原则,现在我们来实践一下。我会在下一部分中使用Clean架构构建一个示例功能。

如何开始写Clean应用?

我已经写好了一个样板项目,里面把准备工作做好了。这相当于是一个Clean的底层包,可以直接在它的基础上进行开发。请随意下载、修改。项目包:Android Clean Boilerplate

开始写用例

这一部分会详细说明如何用在样例项目的基础之上以Clean方式进行开发。首先让我们看一下应用的结构,当这只是我的习惯,不需要完全按这个进行。

结构

一般来说一个安卓应用的结构如下:

  • 外层项目包:UI,Storage,Network等等。
  • 中层项目包:Presenter,Converter。
  • 内层项目包:Interactor,Model,Repository,Executor。

看不懂不要紧,下面有具体解释。

外层

外层体现了框架的细节。

UI – 包括所有的Activity,Fragment,Adapter和其他UI相关的Android代码。

Storage – 用于让交互类获取和存储数据的接口实现类,包含了数据库相关的代码。包括了如ContentProvider或DBFlow等组件。

Network – 网络操作。

中层

桥接实现代码与逻辑代码的Glue Code。

Presenter – presenter处理UI事件,如单击事件,通常包含内层Interactor的回调方法。

Converter – 负责将内外层的模型互相转换。

内层

内层包含了最高级的代码,里面都是POJO类,这一层的类和对象不知道外层的任何信息,且应能在任何JVM下运行。

Interactor – Interactor中包含了解决问题的逻辑代码。这里的代码在后台执行,并通过回调方法向外层传递事件。在其他项目中这个模块被称为用例Use Case。一个项目中可能有很多小Interactor,这符合单一职责原则,而且这样更容易让人接受。

Model – 在业务逻辑代码中操作的业务模型。

Repository – 包含接口让外层类实现,如操作数据库的类等。Interactor用这些接口的实现类来读取和存储数据。这也叫资源库模式Repository Pattern。

Executor – 通过Worker Thread Executor让Interactor在后台执行。一般不需要修改这个包里的代码。

以下是例子

在这个简单例子中,我们的use case是在应用启动时读取数据库中的欢迎语句并展示。下面演示如何编写代码包让use case运行起来。

  • presentation包
  • storage包
  • domain包

前两个包属于外层,最后一个包属于内层(核心层)。

presentation包负责将信息展示在屏幕上,而且包含整个MVP栈,即同时包含UI和presenter这两个属于不同层的组件。下面上码。

写一个内层的Interactor

你可以从任何一层开始编写,我建议从内层的逻辑代码写起。因为逻辑代码写好之后可以测试,不需要activity也可以正常运行。

所以我们先写一个Interactor,这个Interactor包含了处理业务逻辑的代码。**所有的Interactor都应该在后台运行,而不应影响UI展示。**我在这里先编写一个WelcomingInteractor。

public interface WelcomingInteractor extends Interactor { 
    interface Callback { 
        void onMessageRetrieved(String message);
        void onRetrievalFailed(String error);
    } 
}

Callback负责与主线程的UI组件联通。将它放在WelcomingInteractor中可以避免给所有Callback接口起不同的名字而又能将它们有效区分。而后我们要实现获取消息的逻辑。现在已经有一个接口MessageRepository用于获取数据:

public interface MessageRepository { 
    String getWelcomeMessage();
}

现在我们可以用业务逻辑代码来实现Interactor接口了。注意要实现AbstractInteractor接口,这样代码就会在后台执行了。

public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {
    ...
    private void notifyError() {
        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onRetrievalFailed("Nothing to welcome you with :(");
            }
        });
    }
    
    private void postMessage(final String msg) {
        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onMessageRetrieved(msg);
            }
        });
    }               

    @Override
    public void run() {

        // 获取消息
        final String message = mMessageRepository.getWelcomeMessage(); 

        // 检查是否获取失败
        if (message == null || message.length() == 0) {
    
            // 在主线程中通知错误
            notifyError();

            return;
        }

        // 已成功获取消息,通知UI
        postMessage(message);
    }
}

这段代码获取了数据,并向UI层发送数据或报错。这里通过Callback向UI发送信息,这个Callback扮演的是presenter的角色。这段代码是逻辑的核心,其他代码都是依赖框架的。看一下这个类的引用:

import com.kodelabs.boilerplate.domain.executor.Executor;
import com.kodelabs.boilerplate.domain.executor.MainThread;
import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor;
import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor;
import com.kodelabs.boilerplate.domain.repository.MessageRepository;

可以看到,没有和Android相关的类库,这就是Clean架构的好处。还有就是写逻辑代码时不需要关心UI或数据库,只需要调用外层实现的Callback的回调方法。

测试Interactor

现在不需要模拟器也可以运行这段代码了,我们编写一个JUnit测试来确保这段代码运行正常。

@Test
public void testWelcomeMessageFound() throws Exception {

    String msg = "Welcome, friend!";

    when(mMessageRepository.getWelcomeMessage()).thenReturn(msg);

    WelcomingInteractorImpl interactor = new WelcomingInteractorImpl(
        mExecutor,
        mMainThread,
        mMockedCallback,
        mMessageRepository
        );
    interactor.run();

    Mockito.verify(mMessageRepository).getWelcomeMessage();
    Mockito.verifyNoMoreInteractions(mMessageRepository);
    Mockito.verify(mMockedCallback).onMessageRetrieved(msg);
}

重复一遍,Interactor根本不知道它在Android环境下运行。

编写presentation层

Presentation层在Clean架构中属于外层的范围,它依赖于框架,包含了UI展示的代码。我们用MainActivity类在应用启动时展示欢迎信息。

首先编写Presenter和View的接口。View只需要展示欢迎信息。

public interface MainPresenter extends BasePresenter {
    interface View extends BaseView {
        void displayWelcomeMessage(String msg);
    }
}

那怎么在App启动时运行Interactor呢?所有和View无关的代码都写进Presenter类中。这样可以实现关注分离(Separation of Concerns)并能避免Activity过于复杂。这些代码包括和Interactor交互的代码。

在MainActivity中重写onResume()方法。

@Override
protected void onResume() {
    super.onResume();

    // 在活动resume时开始获取数据
    mPresenter.resume();
}

所有的Presenter在继承BasePresenter时都要实现resume()方法。我们在MainPresenter的onResume()方法中启动Interactor。

@Override
public void resume() {

    mView.showProgress();

    // 初始化Interactor
    WelcomingInteractor interactor = new WelcomingInteractorImpl(
            mExecutor,
            mMainThread,
            this,
            mMessageRepository
    );

    // 执行interactor
    interactor.execute();
}

execute()方法会在后台线程中调用WelcomingInteractorImpl类的run()方法。run()方法的实现可以看上文写一个内层的Interactor部分。

你可能已经发现Interactor很像AsyncTask,都是提供所有需要的东西然后运行。那为什么不用AsyncTask呢?因为AsyncTask是Android的代码,需要模拟器来运行与测试。

在上面的代码中我们给Interactor传入了下列属性:

  • ThreadExecutor对象:用于在后台线程运行Interactor。我喜欢将这个类设计成单例。这个类属于domain包,不需要在外层实现。
  • MainThreadImpl对象:用于在主线程中执行Interactor的Runnable对象。在依赖框架的外层代码中我们可以访问主线程,所以这个类要在外层实现。
  • 我们传入this是因为MainPresenter也是一个Callback对象,Interactor要通过Callback来更新UI。
  • 我们传入实现了MessageRepository接口的WelcomMessageRepository对象让Interactor使用。下面会讲到WelcomMessageRepository。

为什么this也是Callback呢?因为MainActivity的MainPresenter实现了Callback接口:

public class MainPresenterImpl extends AbstractPresenter implements MainPresenter,
    WelcomingInteractor.Callback {

我们就是这么监听Interactor的事件的。下面是MainPresenter的代码:

@Override
public void onMessageRetrieved(String message) {
    mView.hideProgress();
    mView.displayWelcomeMessage(message);
}

@Override
public void onRetrievalFailed(String error) {
    mView.hideProgress();
    onError(error);
}

在代码段中我们看到的View其实就是实现了MainPresenter.View接口的MainActivity:

public class MainActivity extends AppCompatActivity implements MainPresenter.View {

View用于展示消息:

@Override
public void displayWelcomeMessage(String msg) {
    mWelcomeTextView.setText(msg);
}

Presentation层的东西就这么多了。

编写Storage层

repository中的接口就在storage层实现。所有与数据库相关的代码都在这里。资源库模式下数据的来源是不确定的,意思是逻辑代码不关心数据的来源,不论是数据库、服务器还是文件。

你可以用ContentProvider或DBFlow等ORM工具处理更复杂的数据。如果你需要从网络获取数据那你可以用Retrofit。如果你只需要基本的键值对存储那你可以用SharedPreferences。不管怎样,一定要选对工具。

这里我们的数据库不是真正的数据库,只是一个模拟了延迟的一个很简单的类。

public class WelcomeMessageRepository implements MessageRepository {
    @Override
    public String getWelcomeMessage() {
        String msg = "Welcome, friend!"; 
        // 模拟网络/数据库延迟
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return msg;
    }
}

WelcomingInteractor可能以为延迟是网络或其他原因造成的,但它并不关心,它只需要数据提供者实现了MessageRepository接口。

总结

详细代码请看这个git repo。总结一下各个类的触发顺序:

MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity

控制流的顺序:

Outer — Mid — Core — Outer — Core — Mid — Outer

在一个use case中多次访问外层很正常。比如当你要显示、存储加访问网络,你的控制流会访问外层至少三次。

Debezium 性能调优实战:吞吐量、延迟与资源平衡 摘要: 本文分享了团队如何通过系统性调优将Debezium从初始3,200 TPS/2.8秒延迟提升至15,200 TPS/210ms延迟的实战经验。优化涵盖四个层面:1)Debezium连接器配置(批处理、快照并行、消息精简);2)Kafka分区策略与Avro序列化缓存;3)JVM改用ZGC及OS参数调优;4)MySQL的binlog与I/O优化。关键发现包括分区数黄金公式、sync_binlog调整带来56%的I/O负载下降,以及避免盲目增大队列内存等陷阱。最终实现吞吐量提升4.75倍,并强调性能调优需 阅读详情

相关推荐

NXP ChessDE的安装和激活

如果客户申请的是试用版,那么一份为期 30 天的 License 将会发送申请人的账号或邮箱中,客户使用该文件进行安装即可。② 将“VENDOR snpslmd /path/to/snpslmd”中的“/path/to/snpslmd”替换成 snpslmd.exe 实际存放路径(位于 SCL 安装目录下的 win32\bin 文件夹中,默认安装的情况下就是“VENDOR snpslmd C:\Synopsys\SCL\<version>\win32\bin\snpslmd.exe”)。

weixin_47351160的博客 2043

android clean 框架,clean架构

1、clean架构简介clean架构相信大家早有耳闻,毕竟是Bob大叔的心血之作,最近又把http://five.agency/blog/android/ 几篇关于clean 架构的文章拿出来读读加深了对该架构的一些理解。clean架构又称“洋葱架构”,这个是由于它的架构示意图得名的删除一些android项目中使用不到的东西加上我们使用的东西看起来如下图2、核心概念从最抽象的核心到细节的边缘Ent...

weixin_35458961的博客 1905

Windows 10/11下彻底卸载Chrome的终极指南(附一键清理注册表脚本)

本文提供了一份在Windows 10/11系统中彻底卸载Google Chrome浏览器的终极指南。针对标准卸载后用户数据与注册表项残留的问题,文章详细介绍了从应用程序层、用户数据层到系统集成层的三层深度清理方法,并重点提供了安全高效的注册表清理脚本,确保浏览器所有痕迹被完全移除,解决重装冲突并释放磁盘空间。

elastic6hunter的博客 782

Clean 架构下的现代 Android 架构指南

Clean 架构是 Uncle Bob 提出的一种软件架构,Bob 大叔同时也是 SOLID 原则的命名者。

大模型训练 1514

测试dali协议的软件,DALI2.0和DALI1.0完美兼容同一个模块,测试每项可过协议

DALI2.0和DALI1.0完美兼容同一个模块,测试每项可过协议深圳聚积智能 夏先生 13246760327 QQ 349098308■IEC62386-102(V2.0)通过测试内容IEC62386-102/2.1/Test preambleIEC62386-102/3.1/Polarity testIEC62386-102/3.2/Maximum and minimum system v...

weixin_35343788的博客 1351

整合WGCNA、机器学习和孟德尔随机化

(G)通过蛋白质免疫印迹法(Western blot)测定的 UT-B(由 SLC14A1 编码)、ARHGEF38、NEFH、MSMB、KRT23 和 KRT15 在前列腺癌肿瘤组织和癌旁非癌组织中的蛋白质表达水平。本研究利用来自基因表达综合数据库(GEO)的数据集筛选差异表达基因(DEGs),并采用加权基因共表达网络分析(WGCNA),以识别在模块中与前列腺癌高度相关的驱动基因。(F)蓝绿色模块中的模块成员关系(MM)与前列腺癌(PCa)的基因显著性(GS)的散点图。(D)6 个基因之间的相关性分析。

多组・学・多组学 1137

Android MVVM+Clean架构简介

一篇文章让你明白Android开发中MVVM的Clean架构及其优缺点,无废话。

左大侠的博客 2483

802.1x之EAP-TLS测试方法

CA证书一般是由权威的认证机构发出,用来验证该机构签名的证书,持有CA证书的设备可以验证对端的证书是否由自己信任的机构颁发的。在我们内部使用证书时,我没可以自己充当CA认证机构,管理私钥和CA证书,使用他们签名服务器、客户端证书。2、使用证书请求文件server_csr.pem、CA私钥、CA公钥生成服务器证书(正常情况下自己不拥有CA私钥,是把证书请求文件发给认证机构,由认证机构返回证书)进行802.1x EAP-TLS认证时需要IPC、服务器分别存储CA证书、设备证书、私钥,通过交换证书实现身份验证。

写作是为了更好的思考 2163

架构实战:用 Clean 和 SOLID 守护你的 Android 架构

摘要: Android开发中,忽视架构设计会导致代码臃肿、难以维护。关注点分离是核心原则,需将应用分为展示层(UI)、领域层(业务逻辑)和数据层(数据源)。以电商订单页为例,若将网络请求、业务逻辑和UI操作全塞进Activity,会导致代码脆弱、难测试且无法复用。例如,后端迁移至GraphQL或适配Wear OS时,紧耦合的代码将被迫重构。采用Clean Architecture与SOLID原则分层设计,能提升扩展性、维护性,避免需求变更引发的灾难性重构。

移动端开发干货分享 1899

OpenWRT软件源更换保姆级教程:从国内镜像到官方源一键切换(含常见报错解决)

本文提供OpenWRT软件源更换的详细教程,涵盖从国内镜像源到官方源的切换方法,并深度解析常见报错解决方案。针对国内网络环境,指导用户如何精准定位系统信息、备份配置、选择并配置清华等国内镜像源,以及处理签名验证失败、依赖缺失、404错误等典型问题,旨在提升软件包管理效率与稳定性。

weixin_29229261的博客 1938

Allegro PCB设计实战:5分钟搞定DDR信号线的Match Group等长规则设置

本文详细介绍了在Allegro PCB设计中,如何快速为DDR信号线设置Match Group等长规则。通过实战演练,解析了从理解相对传输延迟原理、在规则管理器中创建Match Group,到设置合理容差和利用动态绕线指示的全流程,帮助工程师在5分钟内高效完成高速信号时序约束,提升设计可靠性与效率。

gg901的博客 1053

Android应用架构演进之路:从MVC、MVP到MVVM与Clean Architecture

架构设计没有绝对的银弹,只有最适合当前业务场景的选择。对于绝大多数项目:从的模式开始,这是目前投入产出比最高的架构。对于大型复杂项目:在MVVM的基础上引入的分层思想,能让你的应用在未来的迭代中保持清晰和健壮。

m0_65382359的博客 1360

TC3XX - MCAL知识点(四十二):AURIX™ TC3xx实现多相电机需要多核配合使用的场景与实操分析

本文探讨了多相电机控制系统中的关键技术问题,重点分析了多核处理器在电机控制中的应用。针对十二相电机等复杂应用场景,提出了同步采样、多核协同等解决方案。文章详细阐述了相电流同步采样原理及EBTresos配置方法,分析了ADC硬件触发机制和多核配合策略。通过Core0和Core1的协同工作设计,解决了位置信息采集与FOC算法执行的时序问题,并提供了代码实现示例。实验验证了采用ATOM0_7同时触发两个核ADC中断的可行性,为多相电机控制系统的实现提供了技术参考。

梦想技术家 497

Clean架构与MVVM:助你打造优质Android应用

Android应用程序开发可能具有挑战性,特别是在创建可扩展和可维护的代码以适应不断变化的需求和用户需求方面。为了解决这个挑战,开发者通常依赖于软件架构模式,为代码组织和关注点分离提供了坚实的基础。在Android开发中,两种流行的架构模式是和Clean架构。这两种方法都强调模块化、可测试性和关注点分离,但它们在具体实施细节上有所不同。在本文中,我们将探讨如何结合MVVM和Clean架构来创建健壮且易于测试、扩展和维护的Android应用程序。我们还将介绍使用这种强大组合的最佳实践和实现技巧。

虎哥LoveDroid 1585

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

使用Clean架构开发Android应用详细指南

android开发下的clean架构

失踪的使节的博客 622

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Android Clean Architecture 开源项目教程:构建可维护的Android应用架构

你是否遇到过这样的困境:随着业务增长,Android应用代码变得臃肿不堪,模块间耦合严重,测试难以进行,新功能开发举步维艰?传统的MVC(Model-View-Controller)架构在复杂应用中往往力不从心,导致代码维护成本急剧上升。 本文将带你深入解析Android Clean Architecture开源项目,掌握构建可维护、可测试、可扩展的Android应用架构的核心技术。 ## C...

gitblog_00549的博客 415

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Gallery架构设计解析:Clean Architecture在Android应用中的最佳实践

Gallery是一款基于Jetpack Compose开发的Android媒体库应用,采用Clean Architecture架构模式实现了高度解耦的代码结构。本文将深入剖析Gallery如何通过分层设计实现业务逻辑与UI组件的分离,为Android开发者提供可复用的架构设计思路。 ## Clean Architecture核心思想与项目结构 Clean Architecture强调关注点分离

gitblog_00506的博客 507

探索未来安卓开发新境界:基于Kotlin的MVI清洁架构应用

探索未来安卓开发新境界:基于Kotlin的MVI清洁架构应用 在日新月异的移动开发领域,一款设计精良、性能卓越的应用框架往往能成为开发者手中的利剑。今天,我们要介绍的是一款名为Android Kotlin MVI Clean Architecture APP的开源项目,它以其先进的技术和清晰的设计理念,为安卓开发者们开辟了一条高效开发之路。 项目介绍 Android Kotlin MVI Clea...

gitblog_00224的博客 921

[译] Android 架构:Part 3 —— 应用 Clean Architecture

原文地址:https://listenzz.github.io/android-architecture-part-3-applying-clean-architecture-android 到目前为止,在这个系列中,我们已经讲解了一些初学者易犯的错误,以及过了一遍Clean Architecture。在这最后一部分,我们会介绍最后一个难题:标签,或者准确地说,组件。 译者注:看完了这一...

dodod2012的专栏 319

Android Clean 架构浅析

该原创文章首发于微信公众号“字节流动” Android 常用的架构主要有 MVC,MVP,MVVM 以及 Clean 。以上架构设计模式没有绝对的优劣之分,关键要看具体的应用场景。更多关于 MVC,MVP 以及 MVVM 介绍可以参考MVC,MVP & MVVM 什么是 Clean 架构? Clean 架构是由 Uncle Bob 在 2012 年发表的, 针对 Web App ...

字节流动 1万+

Weather Compose Clean Architecture 教程

Weather Compose Clean Architecture 教程 项目介绍 Weather Compose Clean Architecture 是一个多模块的安卓应用程序,它运用了现代Android开发中的先进技术和设计模式。此项目采用Jetpack Compose构建UI,实现了清洁架构(Clean Architecture),并结合了MVVM(Model-View-ViewMod...

gitblog_00833的博客 963

Android架构:第三部分-在Android上应用Clean架构 (译)

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34129145的博客 155
上一篇: Android中自定义圆形头像实现步骤及注意事项
下一篇: 教你认清MVC,MVP和MVVM
Happy_Develop_
博客等级 码龄12年 26粉丝 99原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值