
1. 项目概述一个用AI工具“硬肝”出来的国标收藏App最近在做一个项目需要频繁查阅国家标准每次都得打开浏览器登录“国家标准全文公开”网站再在一堆搜索结果里翻找效率实在太低。作为一个懒人我就在想能不能把这些常用的国标都“装”进手机里随时随地离线查看这个念头一起就再也按捺不住了。正好我最近一直在尝试用Cursor这个AI编程工具来提升开发效率想看看它到底能不能胜任一个完整的Android项目。于是我决定用Java语言完全在Cursor的辅助下从零开始构建一个名为“国标全文”的Android应用。它的核心功能很简单搜索、查看、收藏国家标准。你可以把它理解为一个专门为国标打造的、轻量级的“个人图书馆”。这个项目非常适合两类朋友参考一是对Android开发感兴趣想了解如何将AI工具融入实际工作流的开发者二是像我一样有特定垂直领域信息管理需求的普通用户或开发者。虽然最终效果上它和用浏览器收藏夹区别不大但整个从想法到落地的过程尤其是在AI辅助下如何思考、如何拆解、如何调试其中的经验和踩过的坑我觉得比App本身更有价值。接下来我就把这趟“AI协程开发之旅”的完整记录分享给你。2. 核心思路与技术选型为什么是“Cursor Java 原生”在动手之前我花了些时间思考技术栈。这直接决定了开发效率和最终应用的质量。我的核心诉求是快速验证想法、代码可控、应用轻量。2.1 为什么选择Cursor作为主要开发工具首先主开发工具我选择了Cursor。这不是一个简单的IDE而是一个深度整合了AI目前主要是GPT-4的代码编辑器。它的“Agent”模式可以理解整个项目上下文根据你的自然语言描述生成代码、修复错误、甚至重构。对于我这个单人小项目来说它就像一个不知疲倦的结对编程伙伴。注意使用Cursor这类AI工具最关键的是要学会“提问”。你不能只说“给我写个Android App”而要像给一个初级程序员分配任务一样拆解得非常具体。例如“请为MainActivity创建一个RecyclerView用于显示一个包含标准名称和标准号的列表数据源是一个名为Standard的Java Bean列表。”我选择Cursor而不是纯手写或使用其他低代码平台是基于以下几点考量学习与验证我想亲身验证AI在辅助完成一个完整项目时的边界在哪里哪些地方它很强大哪些地方仍需人工干预。效率提升对于样板代码如Adapter、ViewHolder、简单的网络请求封装Cursor能极大减少重复劳动让我更专注于业务逻辑和架构设计。代码解释当我对某段自动生成的代码不理解时可以直接选中它让Cursor解释其工作原理这对于学习Android新API或回顾旧知识非常有帮助。2.2 为什么坚持使用Java而非Kotlin在Android领域Kotlin已是官方首选语言更简洁安全。但我仍然选择了Java原因如下AI训练数据成熟度Cursor背后的模型对Java语料的训练可能更为充分和稳定。在生成复杂逻辑或涉及特定Android SDK时Java代码的准确性和可靠性相对更高减少了因语言特性理解偏差导致的错误。个人与团队的兼容性这个项目带有实验性质我希望生成的代码具有最广泛的兼容性和可读性。Java作为Android开发的“古典语言”其代码结构对于大多数开发者来说一目了然便于后续其他人阅读或接手。聚焦问题本身我的核心目标是验证“AI辅助开发国标App”的流程而不是比较Kotlin与Java的优劣。使用我更熟悉的Java能让我将更多精力放在功能逻辑和与AI的协作上而非语言特性本身。2.3 为什么采用原生开发而非跨平台框架对于这样一个功能相对单一核心是列表、详情、收藏、且对性能要求不高主要是文本渲染的应用跨平台框架如Flutter, React Native似乎也是个选择。但我最终选择了原生Android开发理由很直接工具链契合度Cursor对Android原生开发Java/Kotlin Gradle的支持最为直接和稳定。生成的项目结构、Gradle配置、资源引用等出错概率更低。最小的依赖复杂度原生开发只需要Android SDK和Gradle环境纯粹。引入跨平台框架意味着要管理另一套依赖和构建流程在AI辅助下可能会引入不必要的复杂性。目标明确这个App就是为Android手机设计的没有多端部署的需求。原生开发能提供最稳定、最符合平台习惯的用户体验例如系统的返回手势、菜单弹出效果等。基于以上思考我确定了“Cursor Java 原生Android”这个技术组合。接下来就是如何利用这个组合一步步把App构建出来。3. 项目结构解析与搭建实操一个清晰的项目结构是良好开发体验的基础。在Cursor里我通过聊天窗口直接描述需求来创建文件和目录。3.1 从零创建Android项目骨架我并没有使用Android Studio的向导而是通过Cursor指令来初始化。我给Cursor的提示是“请为我创建一个基本的Android App项目结构使用Java语言最低API级别设为21包名为com.example.gbqw。”Cursor随后生成了一系列文件和配置。核心的app/build.gradle文件内容如下我特别关注了其中的依赖项plugins { id com.android.application } android { namespace com.example.gbqw compileSdk 34 defaultConfig { applicationId com.example.gbqw minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.10.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 testImplementation junit:junit:4.13.2 androidTestImplementation androidx.test.ext:junit:1.1.5 androidTestImplementation androidx.test.espresso:espresso-core:3.5.1 // 后续手动添加的依赖 implementation com.squareup.okhttp3:okhttp:4.11.0 implementation com.google.code.gson:gson:2.10.1 implementation androidx.recyclerview:recyclerview:1.3.2 }实操要点与避坑Gradle版本同步Cursor生成的gradle-wrapper.properties中的Gradle版本可能不是最新的。我手动将其改为distributionUrlhttps\://services.gradle.org/distributions/gradle-8.1.1-bin.zip以匹配我本地的环境JDK 17。如果不匹配Android Studio会一直卡在同步状态。依赖库版本AI可能会推荐较老或版本冲突的库。比如它最初给的是appcompat:1.4.1我根据Android Studio的提示和建议手动升级到了较新的稳定版。永远不要盲目相信AI生成的版本号要结合官方文档和IDE提示进行确认。目录结构Cursor完美创建了src/main/java/com/example/gbqw和src/main/res等标准目录。但对于res下的子目录如layout,drawable可能需要后续明确指令才会创建。3.2 核心代码模块规划根据功能我规划了以下几个核心Java类并通过Cursor逐一创建MainActivity.java应用主入口包含搜索框和国标列表。Standard.java数据模型对应一个国家标准包含属性如id标准号、name标准名称、publishDate发布日期、pdfUrlPDF链接、isFavourite是否收藏。StandardAdapter.javaRecyclerView的适配器负责将Standard列表数据绑定到每一项的视图上。StandardDetailActivity.java显示国标详情的页面内嵌WebView用于加载PDF或HTML详情页。FavouriteActivity.java收藏夹页面展示所有被收藏的标准。DbHelper.javaSQLite数据库帮助类用于持久化存储收藏记录。ApiClient.java网络请求封装类用于从“国家标准全文公开”网站或模拟接口获取数据。这个结构清晰地将数据模型、UI展示、数据持久化和网络层分离开是典型的MVC或更接近MVP模式便于维护和扩展。4. 核心功能实现与AI协作细节有了骨架接下来就是填充血肉。我通过与Cursor的持续对话实现了各个核心功能。这个过程充满了“提问-生成-审查-调试”的循环。4.1 数据抓取与解析模拟网络请求“国家标准全文公开”网站并没有公开的API。因此我的策略是模拟网页搜索请求并解析返回的HTML。这是一个需要谨慎处理的环节。我首先让Cursor为我创建一个ApiClient类使用OkHttp库。最初的提示是“创建一个名为ApiClient的类使用OkHttp发送HTTP GET请求并处理基本的网络异常。”Cursor生成的代码骨架不错但针对具体的网页抓取需要更精细的指令。我进一步描述“请为ApiClient添加一个searchStandards(String keyword)方法。目标网址是https://openstd.samr.gov.cn/bzgk/gb/我们需要模拟其搜索表单的POST请求。表单数据包括pageSize,pageNum,keyWord等参数。请分析该网页的搜索请求并写出对应的Java代码。”重要提示直接抓取和解析非公开API的网站存在法律和道德风险且网站结构一旦变化代码就会失效。本项目仅为技术演示在实际应用中应优先寻找官方API或确保你的行为符合网站的Robots协议和服务条款。这里的方法仅用于说明AI辅助下如何处理此类任务。Cursor尝试生成了代码但它无法实时访问互联网因此给出的请求参数和头部信息可能是过时的。这时就需要开发者自己通过浏览器的开发者工具F12 - Network标签页去实际分析一次搜索请求然后将正确的参数格式是JSON还是form-urlencoded、必要的Headers如User-Agent, Content-Type告诉Cursor。最终经过几次调试searchStandards方法的核心部分如下public ListStandard searchStandards(String keyword) throws IOException { OkHttpClient client new OkHttpClient().newBuilder().build(); // 注意MediaType和RequestBody格式需要根据实际网站分析确定 MediaType mediaType MediaType.parse(application/x-www-form-urlencoded; charsetUTF-8); String requestBody pageSize15pageNum1keyWord URLEncoder.encode(keyword, UTF-8) ...; // 其他必要参数 Request request new Request.Builder() .url(https://openstd.samr.gov.cn/bzgk/gb/searchPage) .post(RequestBody.create(requestBody, mediaType)) .addHeader(User-Agent, Mozilla/5.0 ...) // 模拟浏览器 .addHeader(Content-Type, application/x-www-form-urlencoded; charsetUTF-8) .addHeader(X-Requested-With, XMLHttpRequest) // 常见于Ajax请求 .build(); try (Response response client.newCall(request).execute()) { if (response.isSuccessful() response.body() ! null) { String html response.body().string(); // 调用HTML解析方法 return parseSearchResultHtml(html); } else { throw new IOException(Unexpected code response); } } }接下来是最复杂的部分HTML解析。我让Cursor根据提供的示例HTML片段编写parseSearchResultHtml方法。我首先在浏览器中查看搜索结果的HTML结构然后复制一段包含单个标准条目的HTML代码给Cursor。我的提示是“给定以下HTML片段请编写一个方法使用Jsoup库解析它提取出每个标准的标准号如GB/T 12345-2020、标准名称、详情页链接并封装成Standard对象列表。”Cursor成功地使用了Jsoup的选择器语法如doc.select(div.result-item)来编写解析逻辑。这个过程可能需要多次迭代因为网页的DOM结构可能很复杂需要不断调整CSS选择器路径。实操心得分步验证不要一次性让AI完成整个复杂功能。先让它生成网络请求框架在Postman或单元测试中验证请求是否成功、返回的数据格式是否正确。然后再进行解析。提供明确示例给AI提供真实、具体的输入输出示例是获得高质量代码的关键。模糊的描述只会得到模糊的、可能错误的代码。异常处理AI生成的代码往往对异常处理考虑不周。务必手动添加健壮的网络超时设置、空指针判断、JSON/HTML解析异常捕获等。4.2 本地数据库设计收藏功能的持久化收藏功能要求数据在应用关闭后依然存在。我选择使用Android自带的SQLite数据库并通过Cursor辅助编写DbHelper类。我给Cursor的指令是“创建一个继承自SQLiteOpenHelper的DbHelper类。需要一张favourites表包含以下字段idINTEGER PRIMARY KEY 这里存储国标的标准号如‘GB/T 12345’nameTEXTpublish_dateTEXTpdf_urlTEXTsaved_timeTIMESTAMP DEFAULT CURRENT_TIMESTAMP。并实现插入、删除、查询所有、按id查询的方法。”Cursor准确地生成了onCreate和onUpgrade方法以及对应的CRUD操作。例如插入方法public long addFavourite(Standard standard) { SQLiteDatabase db this.getWritableDatabase(); ContentValues values new ContentValues(); values.put(id, standard.getId()); values.put(name, standard.getName()); values.put(publish_date, standard.getPublishDate()); values.put(pdf_url, standard.getPdfUrl()); long result db.insert(TABLE_FAVOURITES, null, values); db.close(); return result; // 返回-1表示插入失败 }注意事项主键选择我使用国标的标准号作为主键因为它具有唯一性。这避免了同一标准被重复收藏。在插入前可以先查询是否存在。数据库升级Cursor生成的onUpgrade方法通常只是简单删除旧表。在实际项目中如果后续版本需要增加字段必须编写更复杂的迁移逻辑否则用户更新App后会丢失所有数据。在这个Demo中简单处理是可以接受的。线程安全SQLiteOpenHelper内部已经处理了数据库连接池但在多线程环境下频繁开关数据库仍可能有问题。可以考虑使用单例模式封装DbHelper或直接使用Room等更现代的持久化库。这里为了简化未做额外处理。4.3 UI构建主列表与详情页UI部分是我与Cursor协作最顺畅的部分。Android的XML布局和Java活动代码有很强的模式性AI非常擅长。创建主界面布局 (activity_main.xml) 我的提示“创建一个使用ConstraintLayout的布局。顶部有一个EditText搜索框和一个Button搜索按钮。下面是一个RecyclerView用于显示搜索结果列表。每个列表项包含两个TextView分别显示标准名称和标准号并有一个星形ImageButton用于收藏。”Cursor生成的XML代码结构清晰约束关系基本正确。但有时它会对控件ID命名得比较通用如textView1。我会手动将其改为更具语义的名字如textViewStandardName这样在Java代码中引用时更易读。创建列表项适配器 (StandardAdapter.java) 这是体现AI效率的地方。我描述道“创建一个RecyclerView.Adapter数据源是List 。每个ViewHolder包含两个TextView和一个ImageButton。根据Standard对象的isFavourite字段设置ImageButton星星的选中状态。点击星星按钮触发一个回调接口通知Activity进行收藏或取消收藏的操作。”Cursor在几分钟内就生成了一个功能完整的Adapter包括ViewHolder、onBindViewHolder中的逻辑绑定以及一个自定义的OnFavouriteClickListener接口。我只需要稍作调整比如处理按钮点击事件的具体逻辑调用DbHelper以及列表项的点击事件跳转到详情页。创建详情页 (StandardDetailActivity.java) 详情页的核心是一个WebView用于加载国标的PDF文件或HTML页面。我让Cursor“创建一个StandardDetailActivity从Intent中获取Standard对象并在WebView中加载其pdfUrl。需要处理WebView的基本设置如启用JavaScript如果需要以及简单的加载进度显示。”Cursor生成的代码包含了WebViewClient和WebChromeClient的基本设置用于处理页面加载和进度条。一个关键点是有些PDF链接可能需要特殊的处理才能在内置WebView中打开可能需要添加webView.getSettings().setBuiltInZoomControls(true);等设置。4.4 功能串联与事件处理最后一步是将所有模块串联起来。在MainActivity中我需要初始化RecyclerView和Adapter。在搜索按钮点击时调用ApiClient.searchStandards()注意需在子线程执行。将网络返回的数据更新到Adapter。处理Adapter传来的收藏点击事件调用DbHelper进行数据库操作并更新数据源和UI。这里涉及到一个经典问题网络请求必须在子线程进行而UI更新必须在主线程。我让Cursor帮我处理这个问题“在MainActivity中搜索按钮的点击事件里使用AsyncTask或Kotlin协程/RxJava来执行网络请求并在请求成功后在主线程更新RecyclerView的数据。”考虑到项目是JavaCursor选择了AsyncTask。它生成了基本的doInBackground和onPostExecute框架。但我必须提醒它也是提醒自己AsyncTask在Android API 30及以上已被废弃。作为更优解我手动将其替换为ExecutorService和Handler的组合或者更简单地在后续优化中引入RxJava或协程。事件处理的代码片段示例searchButton.setOnClickListener(v - { String keyword searchEditText.getText().toString().trim(); if (!keyword.isEmpty()) { new Thread(() - { try { ListStandard result apiClient.searchStandards(keyword); // 查询数据库为结果集中的每个Standard设置isFavourite状态 for (Standard standard : result) { standard.setFavourite(dbHelper.isFavourite(standard.getId())); } runOnUiThread(() - { adapter.setData(result); adapter.notifyDataSetChanged(); }); } catch (IOException e) { e.printStackTrace(); runOnUiThread(() - Toast.makeText(MainActivity.this, 搜索失败: e.getMessage(), Toast.LENGTH_SHORT).show()); } }).start(); } });至此一个具备搜索、查看、收藏基本功能的国标App就实现了。整个过程大约70%的代码由Cursor根据我的指令生成30%需要我进行调试、参数修正、逻辑补充和优化。AI就像一个强大的代码自动补全和脚手架生成器但项目的灵魂——架构设计、业务逻辑梳理、异常边界处理——仍然牢牢掌握在开发者手中。5. 开发过程中的典型问题与排查实录在AI辅助开发中遇到的问题有时很独特。这里记录几个典型案例和解决思路。5.1 问题一Gradle同步失败提示“Could not resolve ...”现象Cursor生成的build.gradle文件中依赖版本号可能不存在或与当前Gradle版本不兼容导致同步时下载失败。排查步骤检查网络确认网络连接正常可以访问Maven Central等仓库。检查Gradle版本核对gradle/wrapper/gradle-wrapper.properties中的distributionUrl是否与本地环境匹配。我本机是JDK 17就需要Gradle 7.3以上版本。检查依赖版本将Cursor生成的依赖版本如implementation androidx.appcompat:appcompat:1.4.1复制到浏览器中搜索“androidx.appcompat maven”进入官方仓库页面通常是mvnrepository.com或developer.android.com查看该库的最新稳定版版本号。使用IDE建议在Android Studio中将版本号改为1.6.1举例IDE通常会给出智能提示推荐可用的最新版本。解决方案手动将依赖版本更新为IDE推荐或官方文档中列出的稳定版本。对于Gradle插件版本在项目根目录的build.gradle中修改classpath。5.2 问题二运行App后列表为空但网络请求似乎成功了现象搜索后RecyclerView没有显示任何数据但Logcat中没有明显的崩溃日志。排查步骤检查数据源在onPostExecute或更新UI的代码处打日志打印result列表的size确认数据是否真的从网络请求中获取到了。检查解析逻辑如果result.size()为0问题出在HTML解析。在parseSearchResultHtml方法中打印解析前的原始HTML字符串或保存到文件检查其结构是否与提供给Cursor的示例片段一致。网页结构可能已经调整。检查UI绑定如果数据存在检查Adapter的setData方法是否被调用以及notifyDataSetChanged是否在主线程执行。检查RecyclerView布局确认RecyclerView的layout_width和layout_height不是wrap_content且内容为空导致高度为0。通常应设为match_parent或一个固定值。检查Adapter连接确认RecyclerView.setAdapter(adapter)和RecyclerView.setLayoutManager(new LinearLayoutManager(this))这两行代码是否执行。解决方案这是一个典型的“数据流”排查问题。通过添加日志逐步定位问题发生在网络请求、数据解析、数据传递还是UI渲染环节。本例中最常见的原因是网页HTML结构变化导致Jsoup选择器失效需要重新分析网页并更新解析代码。5.3 问题三点击收藏星星按钮图标状态不更新现象点击列表项的收藏按钮数据库操作成功了通过日志确认但星星的选中状态没有实时刷新。排查步骤确认回调触发在Adapter的onBindViewHolder中为星星按钮设置点击监听器并在回调方法里打日志确认点击事件被触发了。确认数据更新在回调方法中确认调用了dbHelper.addFavourite()或dbHelper.removeFavourite()并检查返回值。确认数据源同步这是关键在数据库操作成功后必须更新数据源ListStandard中对应Standard对象的isFavourite属性。否则Adapter在重新绑定时比如滑动列表还是会用旧的数据状态来设置按钮图标。通知局部更新在更新了数据源中特定位置的对象属性后调用adapter.notifyItemChanged(position)来只刷新当前项而不是notifyDataSetChanged()刷新整个列表这样效率更高且没有闪烁。解决方案在Adapter的点击回调接口实现中确保完成以下步骤Override public void onFavouriteClick(int position, Standard standard) { boolean newFavouriteState !standard.isFavourite(); // 1. 操作数据库 if (newFavouriteState) { dbHelper.addFavourite(standard); } else { dbHelper.removeFavourite(standard.getId()); } // 2. 更新数据源 standard.setFavourite(newFavouriteState); dataList.set(position, standard); // 确保列表引用的是同一个对象或者直接修改对象属性 // 3. 通知UI更新 notifyItemChanged(position); }5.4 问题四WebView加载PDF显示空白或下载现象在StandardDetailActivity中WebView加载PDF链接没有显示内容或者直接弹出下载对话框。排查步骤与解决方案检查链接有效性首先确认pdfUrl是一个有效的、可直接在浏览器中打开的PDF链接。添加WebView设置PDF渲染需要额外权限和设置。在onCreate中补充WebSettings webSettings webView.getSettings(); webSettings.setJavaScriptEnabled(true); // 部分PDF查看器需要JS webSettings.setAllowFileAccess(true); webSettings.setDomStorageEnabled(true); // 以下设置对加载PDF至关重要 webSettings.setLoadWithOverviewMode(true); webSettings.setUseWideViewPort(true); webSettings.setBuiltInZoomControls(true); webSettings.setDisplayZoomControls(false);处理下载如果网站策略是直接提供PDF下载WebView可能默认触发下载。我们可以通过WebViewClient的shouldOverrideUrlLoading方法来拦截处理尝试用WebView直接打开。但有些链接确实只能下载。一个更可靠的方案是使用Intent调用系统浏览器或其他PDF阅读器App来打开Intent intent new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(pdfUrl)); startActivity(intent);这就需要权衡用户体验是在应用内浏览可能不稳定还是跳转到外部应用体验割裂。与AI协作的调试技巧当遇到这类平台特定问题时直接问Cursor“Android WebView如何加载PDF”可能得到通用答案。更有效的方法是将具体的错误日志或现象描述清楚例如“在我的StandardDetailActivity中WebView加载一个PDF URL控制台出现了‘net::ERR_UNKNOWN_URL_SCHEME’错误如何解决” AI就能给出更针对性的建议比如检查URL scheme或者添加WebChromeClient来处理文件选择。6. 项目总结与AI辅助开发的真实体会回顾这个项目的开发过程从构思到实现一个可用的APK文件大约用了两个周末的碎片时间。如果没有Cursor我可能需要多花一倍的时间在查找文档、编写样板代码和调试语法错误上。AI辅助编程工具特别是像Cursor这样深度集成的确实是一个强大的“力量倍增器”。我个人最深的三点体会是第一AI是优秀的“执行者”但你是不可或缺的“架构师”和“质检员”。它可以快速生成符合模式的代码块但它不理解你项目的整体架构、业务边界和潜在的风险。例如它不会主动思考网络请求该放在哪个线程不会判断使用标准号作数据库主键是否合理更不会考虑网页抓取的法律合规性。这些关乎项目质量和成败的决策必须由开发者做出。AI生成的代码必须经过严格的审查、测试和重构。第二与AI沟通的能力成了新的关键技能。如何清晰、无歧义地描述需求如何提供有效的上下文如错误信息、代码片段、输入输出示例直接决定了产出代码的质量。这有点像“超级编程”你定义问题、描述接口、制定规则AI负责填充实现细节。学会写好的“提示词”Prompt其重要性不亚于学会写好的代码注释。第三它改变了学习与解决问题的方式。过去遇到问题我的动线是搜索引擎 - 翻阅Stack Overflow - 查官方文档。现在我会先问Cursor“我遇到了XX问题可能的原因有哪些” 它能给出一个结构化的排查清单。或者直接给它看错误日志让它解释含义和解决方案。这极大地加速了调试过程。当然对于它给出的方案尤其是涉及第三方库或复杂机制时我仍然会去查阅官方文档进行二次确认但第一轮的筛选和方向指引AI做得非常高效。最后关于这个“国标全文”App本身它确实只是一个玩具级的应用功能上比不过一个精心整理的浏览器书签文件夹。但它的价值在于它完整地走完了一个想法从诞生到落地的闭环并且验证了在AI工具的辅助下个人开发者可以多么快速地原型化一个垂直领域的工具。如果你也有某个特定场景下的信息管理痛点不妨也试试用这个思路让AI帮你“肝”出一个专属解决方案来。