Android入门练手项目:从零搭建留言板App核心实现与避坑指南
发布时间:2026/9/2 3:39:07 作者:尧图编辑部 阅读量:1,286

简介一份面向Android初学者的留言板小Demo覆盖发表留言、删除留言与动态显示等场景适合希望把Activity、SQLite与列表刷新串起来的读者。示例围绕Activity展开从EditText、Button的UI布局开始完成留言输入、校验、提交与结果提示数据层使用SQLite保存记录并在删除时通过SQL语句定位目标记录、执行删除并同步列表展示层借助Adapter与ListView/RecyclerView实现数据绑定配合notifyDataSetChanged完成增删后的实时刷新。这一过程涵盖了Activity生命周期、事件监听、SQLite增删查改和列表适配器同步等关键知识点同时也涉及删除确认、错误处理与基础交互优化。资源包为RAR格式整体约6.09MB页面未单列文件总数与类型明细以解压后的工程结构为准。已有1196人学习/下载适合想快速上手Android数据存储与界面联动的开发者也可作为课程设计或自学练手素材。 做Android开发这些年我见过太多人问同一个问题想快速入门第一个练手项目做什么好我的答案一直很固定——做一个留言板小demo。别看它名字里带个“小”这个项目把Android开发的骨架全撑起来了Activity、布局文件、RecyclerView列表、适配器、事件监听、数据存储这些入门必须掌握的东西一个都不少。如果你正准备用Android Studio搭建自己的第一个应用或者已经会了一些零散知识点但不知道怎么串起来这个留言板demo就是最适合动手的练手材料。它走通的链路很清晰用户输入一句留言点发送新留言出现在列表顶部重启App之后数据还在。就这一条完整链路足够让你把Android应用从界面到逻辑完整走一遍。我在带新人或者自己练手的时候会反复用这个项目来验证对基础知识的理解程度。有些东西看着简单真上手写才会发现细节一堆。这篇文章就把整个demo从环境准备、界面设计到核心代码、常见坑位全部拆开讲清楚照着走一遍你也能在半小时内跑出一个能用的留言板。1. 这个留言板demo的定位与技术选型1.1 为什么选留言板作为练手项目留言板最吸引我的地方在于它的业务逻辑非常朴素但交互链路极其完整。输入内容、提交数据、刷新列表、持久化存储这四步恰好对应了Android应用开发里最常打交道的四类操作——控件交互、界面更新、数据管理和生命周期处理。比起计算器、待办清单这类同样热门的demo留言板的体验更接近真实产品用户会持续产生内容列表会不断增长你需要考虑新数据怎么插入、老数据怎么保存这种“持续演进”的感觉是静态demo给不了的。从难度梯度上看留言板也足够友好。你不需要一开始就碰网络请求、数据库设计、架构模式这些东西只需要专注在单机本地功能的实现上。等把这个demo完全跑通你对RecyclerView的工作机制、适配器的刷新逻辑、SharedPreferences的存储方式都会形成直观认识这时候再往Room、ViewModel、LiveData那些方向上深入心里就有底了。很多新人一上来就照着大型项目模仿结果被各种抽象概念砸晕最后连一个简单的界面都没跑起来反倒是这种小demo能循序渐进地把基础夯扎实。1.2 开发环境与依赖配置我用的是Android Studio Hedgehog 2023.1.1稳定版AGP版本8.2Kotlin版本1.9.x。这个组合目前比较稳新建工程的时候模板默认就是Kotlin 新版Gradle配置不用手动折腾太多东西。如果你电脑上装的是更高版本的Android Studio也没关系核心代码逻辑是通用的只要AGP和Gradle版本能匹配上就行。项目配置方面我在build.gradle.kts模块级里加了这样几个依赖dependencies { implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) implementation(androidx.recyclerview:recyclerview:1.3.2) }compileSdk用34minSdk用24。minSdk设到24主要是为了省心覆盖了绝大多数现役设备同时不用处理老版本系统上的一些兼容问题。语言选Kotlin而不是Java这个我不过多解释——现在的Android官方文档、社区示例、第三方库几乎都优先支持Kotlin你早晚要转到Kotlin上来不如一开始就用。整个demo涉及的基础依赖就上面三个其他全部用Android SDK自带的能力让关注点完全集中在核心逻辑上。2. 界面布局与数据模型设计2.1 主界面布局的搭建思路留言板的主界面从上到下分三层顶部标题栏、中间留言列表、底部输入栏。我用一个垂直方向的LinearLayout来承载这三部分中间列表部分用RecyclerView并设置layout_weight1让它自动撑满剩余空间。底部输入栏是一个横向LinearLayout左边EditText输入内容右边Button触发发送。布局文件activity_main.xml如下?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:background#F5F6F8 TextView android:idid/tvTitle android:layout_widthmatch_parent android:layout_height48dp android:gravitycenter android:text留言板 android:textSize18sp android:textColor#333333 android:background#FFFFFF / androidx.recyclerview.widget.RecyclerView android:idid/rvMessages android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:padding12dp android:clipToPaddingfalse / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding10dp android:background#FFFFFF EditText android:idid/etInput android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint说点什么... android:maxLines3 android:inputTypetextMultiLine / Button android:idid/btnSend android:layout_widthwrap_content android:layout_heightwrap_content android:text发送 / /LinearLayout /LinearLayout这里有两个细节值得说明。第一为什么列表不用ScrollView嵌套LinearLayout而是用RecyclerView因为RecyclerView自带ViewHolder回收复用机制当留言条数变多时它只创建屏幕范围内可见的item滑动过程中不断复用旧视图性能比ScrollView方案好太多。第二EditText的inputType我设成了textMultiLine这样用户输入长文本时能自动换行同时配合maxLines3限制最大高度避免输入框把列表区域挤压得过于狭窄。2.2 留言数据模型与列表项设计一条留言需要包含两个核心字段留言内容和发布时间。这里我用Kotlin的data class来定义简单清晰data class Message( val content: String, val timestamp: Long )time戳用System.currentTimeMillis()生成保存的是毫秒值展示的时候再按需格式化。这样存的是原始数据想显示成什么格式都很方便不会在存储阶段就把展示逻辑写死。列表项item_message.xml相对简单我用了垂直方向的LinearLayout上面显示留言内容下面显示格式化后的时间?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding12dp android:layout_marginBottom10dp android:background#FFFFFF TextView android:idid/tvContent android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize16sp android:textColor#222222 / TextView android:idid/tvTime android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop6dp android:textSize12sp android:textColor#999999 / /LinearLayout这里我直接把背景色设成了白色配合根布局的浅灰背景每条留言自然形成了卡片式分隔效果。如果你想要更精致的圆角卡片可以专门写一个shape drawable作为背景这里为了demo简洁就没做这层封装。3. 核心代码实现与完整流程3.1 适配器实现与列表刷新RecyclerView的适配器是整个demo里最容易写错的地方。我先把完整代码贴出来然后逐个方法解释class MessageAdapter( private val messages: MutableListMessage ) : RecyclerView.AdapterMessageAdapter.ViewHolder() { class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val tvContent: TextView itemView.findViewById(R.id.tvContent) val tvTime: TextView itemView.findViewById(R.id.tvTime) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_message, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val message messages[position] holder.tvContent.text message.content holder.tvTime.text formatTime(message.timestamp) } override fun getItemCount(): Int messages.size private fun formatTime(timestamp: Long): String { val sdf SimpleDateFormat(MM-dd HH:mm, Locale.getDefault()) return sdf.format(Date(timestamp)) } }写Adapter的时候最容易犯的错误是搞混onCreateViewHolder和onBindViewHolder的分工。onCreateViewHolder只在需要创建新视图时调用负责把布局文件变成View对象onBindViewHolder则负责把数据绑定到视图上。ViewHolder存在的意义就是缓存item里的子View引用避免每次绑定数据都要重新findViewById一遍这在高频刷新时会明显影响性能。另外注意我拿到的是外部传入的同一个MutableList引用所以外部List增加数据后只要调用notify相关方法Adapter就能感知并刷新界面。3.2 发布留言与持久化存储MainActivity是整个demo的调度中心。初始化、加载历史数据、绑定点击事件、保存数据都在这里完成。看代码class MainActivity : AppCompatActivity() { private lateinit var rvMessages: RecyclerView private lateinit var etInput: EditText private lateinit var btnSend: Button private lateinit var adapter: MessageAdapter private val messages mutableListOfMessage() private val prefs by lazy { getSharedPreferences(message_board, MODE_PRIVATE) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) rvMessages findViewById(R.id.rvMessages) etInput findViewById(R.id.etInput) btnSend findViewById(R.id.btnSend) loadMessages() adapter MessageAdapter(messages) rvMessages.layoutManager LinearLayoutManager(this) rvMessages.adapter adapter btnSend.setOnClickListener { addMessage() } } private fun addMessage() { val content etInput.text.toString().trim() if (content.isEmpty()) { Toast.makeText(this, 留言内容不能为空, Toast.LENGTH_SHORT).show() return } messages.add(0, Message(content, System.currentTimeMillis())) adapter.notifyItemInserted(0) rvMessages.scrollToPosition(0) etInput.setText() saveMessages() } private fun saveMessages() { val jsonArr JSONArray() for (msg in messages) { val obj JSONObject() obj.put(content, msg.content) obj.put(timestamp, msg.timestamp) jsonArr.put(obj) } prefs.edit().putString(messages, jsonArr.toString()).apply() } private fun loadMessages() { val jsonStr prefs.getString(messages, []) val jsonArr JSONArray(jsonStr) for (i in 0 until jsonArr.length()) { val obj jsonArr.getJSONObject(i) messages.add(Message(obj.getString(content), obj.getLong(timestamp))) } } }先说数据存储。demo阶段我选了SharedPreferences JSON字符串的方案没有上SQLite或者Room。原因很直接留言条数不多时把整个列表序列化成一个字符串存起来实现成本最低读出来直接JSONArray解析就能恢复。用SQLite当然也可以但需要自己写数据库帮助类处理升级、事务、游标关闭这些细节对新手来说概念负担太重。Room更是要把注解、DAO、数据库类全部建立起来作为demo会喧宾夺主。等你要做正式的留言板产品再换成Room完全来得及核心界面逻辑不用大改。再说列表刷新。这里我用的是notifyItemInserted(0)而不是notifyDataSetChanged()同时把新留言插入到List的最前面。这样新发的留言会出现在顶部配合rvMessages.scrollToPosition(0)用户能立刻看到自己刚发的内容。notifyItemInserted比notifyDataSetChanged精细得多它只通知Adapter在位置0插入了一条数据系统会走item动画不会把整个列表全部重绘。这个细节在数据量小的时候看不出来但养成用细粒度通知的习惯对你后面做复杂列表会很有帮助。3.3 启动加载历史留言loadMessages()在onCreate里执行顺序放在创建Adapter之前。这样保证messages列表在初始化Adapter时已经有了数据Adapter的getItemCount不会返回0。有个小坑SharedPreferences的getString方法如果对应key不存在返回的是null所以我在调用prefs.getString(messages, [])时给了默认值[]避免JSONArray的构造函数拿到null引发崩溃。解析JSON的时候org.json这套API虽然写起来没有Gson那么简洁但它不依赖任何第三方库Android SDK自带用来存留有一个轻量数据集合非常合适。如果你项目里本来就已经引入了Gson用Gson更省事直接TypeToken把JSON字符串反序列化成List代码还能少一半。但作为一个刻意保持最小依赖的demo我用原生JSONArray的方式也能让初学者看清序列化和反序列化的完整数据流。安卓界还有一个细节我建议顺手加上给EditText设置监听当用户点击键盘上的发送键时也能提交留言这比只点按钮更符合聊天产品的手感。Kotlin写起来很简洁etInput.setOnEditorActionListener { _, actionId, _ - if (actionId EditorInfo.IME_ACTION_SEND) { addMessage() true } else { false } }注意给EditText设置android:imeOptionsactionSend并把inputType设为textMultiLine时部分输入法不会显示发送键这是输入法的自身行为不影响通过按钮提交。4. 常见问题与避坑经验4.1 列表不更新数据“没生效”这是RecyclerView新手最容易踩的坑。现象是添加数据后列表界面毫无变化或者要滑动一下才出现新item。原因基本都出在忘了调notify相关方法。你改了messages集合只是改了内存里的数据Adapter并不知道数据变了必须主动通知它。记住一句话修改List之后一定要调用adapter.notifyDataSetChanged()或者更细粒度的notifyItemInserted() / notifyItemRemoved()。还有另一种情况你调用了notifyItemInserted但位置对不上。比如数据插到了List末尾却通知插入在位置0列表就会显示异常甚至崩溃。排查这类问题时我习惯在onBindViewHolder里打一个Log把position和数据内容打出来一眼就能看出谁和谁没对上。4.2 输入法弹出遮挡住输入框底部输入栏比较容易被软键盘顶上来或者直接盖住这取决于AndroidManifest里对windowSoftInputMode的配置。我推荐用adjustResizeactivity android:name.MainActivity android:windowSoftInputModeadjustResize /adjustResize会在键盘弹出时压缩Activity的可用高度让布局重新布局底部输入栏自动顶到键盘上方。如果你用adjustPan系统会把整个窗口上移在输入框位于底部时表现尚可但界面顶部的标题会被推出去体验不太好。不过说实话我在一些深度定制的国产ROM上遇到过adjustResize不生效的情况键盘弹出时布局纹丝不动输入框被完全遮住。应急方案是监听键盘弹出事件动态给列表底部加一个padding或者直接把输入栏平移上去。要做这个监听可以用ViewTreeObserver.OnGlobalLayoutListener监听布局变化对比根视图的高度差判断键盘状态。这个方案具备一定复杂度demo阶段先记着有这个坑就好。4.3 SharedPreferences读写容易踩的坑第一个坑是空字符串。如果读取出来的jsonStr是空字符串直接JSONArray(jsonStr)会抛异常。所以读取时要做好兜底要么在读的时候判断isEmpty要么默认值就传[]。第二个坑是apply()和commit()的选择。apply是异步写磁盘不会阻塞主线程但写入时机不确定commit是同步写会阻塞主线程直至写入完成。demo里数据量小用apply完全没问题千万别在主线程用commit频繁提交大块数据。还有一个坑级别比较隐蔽如果你在多个位置修改SharedPreferences记得用同一个key和同一个SharedPreferences实例。我见过有项目在Application里创建了一个SharedPreferences在某个Activity里又用另一个文件名创建结果数据读出来永远对不上。统一集中在同一个管理类里操作能省掉很多玄学问题。5. 留给你的扩展思路5.1 从demo到完整应用的三个方向这个留言板跑通之后再往下走的方向就很清晰了。第一把数据层换掉引入Room数据库加一个Repository仓库类管理数据源结合ViewModel和LiveData做响应式更新这一步是往正规架构迈进的关键。第二加交互能力长按留言可以删除点击留言可以编辑再给每条留言加上头像和昵称整体就更像一个社区产品。第三接入网络用Retrofit对接后端接口让留言能够跨设备同步这个demo就真正变成一个线上应用了。这三个方向难度递增你可以按自己的精力选一个深入。5.2 我在这个demo里体会最深的一点做这个小demo最让我感慨的不是某个API怎么调用而是“完整走通一条链路”的价值。很多新手学Android今天看一个RecyclerView教程明天看一个数据存储文章每个知识点都“看懂了”但从来没把这些串联起来跑一遍。这个留言板虽然只有几百行代码却逼着你把界面、数据、适配器、存储全流程亲手走一遍所有知识点在动手过程中自然打通。我强烈建议你不要只复制粘贴代码而是每写完一个模块停下来想想如果去掉某一行会怎样改一个参数又会怎样。把demo折腾坏了再修好学到的东西比顺利跑通多一倍。说到底练手项目的价值不在于“多高级”而在于让你用自己的手验证自己的理解。本文还有配套的精品资源点击获取