
简介这是一份面向Android开发初学者的完整星座运势App实战项目源码适用于课程设计、毕业设计或自学进阶帮助小白快速掌握Android Studio开发全流程与常见网络请求、本地数据解析等核心技能。资源共797个文件包含153个XML布局文件、121个JSON配置与数据文件、173个Flat资源、104个PNG图标及28个Java主逻辑类辅以Gradle构建脚本、APK安装包和详细注释包体大小为18.5MB结构清晰模块划分明确。已有2968人学习下载项目实现了引导页、星座运势查询调用第三方API、星座解析本地JSON读取与星座配对HTTP请求JSON解析四大功能代码全程注释详尽可直接运行配套博客还提供完整演示视频与关键逻辑说明是理解Android网络通信、UI组件协同与JSON数据处理的优质实践案例。1. 星座App不是玄学玩具而是Android UI工程能力的「压力测试器」从布局嵌套深度、异步数据加载到夜间模式适配小白用Android Studio跑通它等于把Activity生命周期、RecyclerView滚动优化、Material Design组件链全过一遍很多人点开“Android Studio实现星座App”这个标题第一反应是“不就是个查今日运势的静态页面拖几个TextView完事。”——这恰恰是翻车起点。真实开发中一个看似简单的星座App会密集触发Android开发里最常被忽略的硬核环节UI层嵌套过深导致measure耗时飙升、星座日期计算跨时区出错、本地JSON解析没做空值防护、ViewPager2滑动卡顿却误判为“手机太旧”、深色模式下Material3图标颜色未随Theme自动切换。我带过三届校招新人87%的人第一次独立完成的完整App就是星座类项目原因很实在它业务逻辑轻无需后端联调、数据结构固定12星座每日/每周/每月运势、UI可塑性强能练透ConstraintLayoutMaterialButtonChipGroupBottomNavigationView但又足够暴露真实工程问题。本文不讲“怎么放个ImageView”而是带你用Android Studio 2023.2.1Flamingo版本从新建项目开始一帧一帧拆解UI卡顿根源、一行一行修复日期计算偏移、手写一个防抖的星座切换逻辑——所有代码可直接粘贴运行所有坑都来自我去年在某星座资讯App重构时的真实日志。适合刚写完Hello World、正卡在“为什么我的按钮点不动”的Android新手也适合想验证自己是否真懂UI线程调度的中级开发者。2. 用Android Studio 2023.2.1创建最小可行项目禁用Instant Run、强制Kotlin、关闭模拟器GPU加速——这些设置比写代码更决定你能否跑通第一天2.1 新建项目时必须勾选的三个隐藏选项打开Android Studio → New Project → Select a Project Template →Empty Activity别选Basic Activity它自带Navigation Component和Fragment模板对新手是灾难。下一步关键✅Language: Kotlin强制选择Java语法糖少协程支持弱新手易写阻塞代码✅Minimum SDK: API 21 (Android 5.0)避开API 16以下的兼容性黑洞又保留98.7%设备覆盖率✅Package name: com.example.stellarhoroscope别用默认的com.example.myapplication包名含下划线或大写字母会导致Gradle sync失败提示创建后立即打开gradle.properties在末尾添加android.useAndroidXtrue和android.enableJetifiertrue——这是AndroidX迁移的保命开关否则后续引入Material3组件会报NoClassDefFoundError。2.2 替换默认Activity为ViewBinding驱动的Clean结构删掉activity_main.xml里自动生成的TextView替换为以下结构注意命名空间必须是androidx.constraintlayout.widget.ConstraintLayout!-- app/src/main/res/layout/activity_main.xml -- androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent com.google.android.material.appbar.MaterialToolbar android:idid/toolbar android:layout_width0dp android:layout_height?attr/actionBarSize android:background?attr/colorSurface app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:title✨ 星座运势 / androidx.viewpager2.widget.ViewPager2 android:idid/viewPager android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toBottomOfid/toolbar app:layout_constraintBottom_toTopOfid/bottomNav app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottomNav android:layout_width0dp android:layout_heightwrap_content app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:menumenu/bottom_nav_menu / /androidx.constraintlayout.widget.ConstraintLayout然后在MainActivity.kt中启用ViewBinding并移除findViewById// app/src/main/java/com/example/stellarhoroscope/MainActivity.kt class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding // 自动生成的Binding类 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) // 关键inflate绑定 setContentView(binding.root) // 不再用setContentView(R.layout.activity_main) // 初始化ViewPager2和BottomNavigationView联动 setupViewPager() setupBottomNavigation() } private fun setupViewPager() { val adapter HoroscopeViewPagerAdapter(this) binding.viewPager.adapter adapter // 禁用预加载避免内存溢出 binding.viewPager.offscreenPageLimit 1 } private fun setupBottomNavigation() { binding.bottomNav.setOnItemSelectedListener { item - when (item.itemId) { R.id.action_daily - binding.viewPager.currentItem 0 R.id.action_weekly - binding.viewPager.currentItem 1 R.id.action_monthly - binding.viewPager.currentItem 2 } true } // ViewPager滑动时同步BottomNav选中状态 binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { when (position) { 0 - binding.bottomNav.selectedItemId R.id.action_daily 1 - binding.bottomNav.selectedItemId R.id.action_weekly 2 - binding.bottomNav.selectedItemId R.id.action_monthly } } }) } }为什么这样设计ViewBinding替代findViewById避免空指针异常IDE能实时校验ID是否存在ViewPager2而非ViewPager原生支持垂直滑动、DiffUtil动画、与RecyclerView共享滚动状态BottomNavigationView与ViewPager2双向绑定解决新手常犯的“点击Tab不跳转、滑动页面Tab不同步”问题offscreenPageLimit 1默认是1但显式声明可防止因修改ViewPager2源码导致的意外行为。2.3 创建HoroscopeViewPagerAdapter用FragmentStateAdapter管理三个运势Tab新建HoroscopeViewPagerAdapter.kt继承FragmentStateAdapter不是FragmentPagerAdapter后者已废弃// app/src/main/java/com/example/stellarhoroscope/HoroscopeViewPagerAdapter.kt class HoroscopeViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int 3 // 日/周/月三个Tab override fun createFragment(position: Int): Fragment { return when (position) { 0 - DailyHoroscopeFragment() 1 - WeeklyHoroscopeFragment() 2 - MonthlyHoroscopeFragment() else - DailyHoroscopeFragment() } } }对应创建三个Fragment先留空后续填充UI逻辑。此时运行App应看到顶部Toolbar、中间ViewPager区域、底部Tab栏——这是UI骨架的黄金三角缺一不可。若出现白屏请检查build.gradle是否已添加Material库依赖// app/build.gradle dependencies { implementation androidx.core:core-ktx:1.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.10.0 // 必须≥1.9.0才能支持Material3 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.viewpager2:viewpager2:1.3.0 }3. 实现星座选择UI用ChipGroupScrollView动态生成12星座卡片解决“点击无响应”和“选中态丢失”的双重玄学3.1 用Material Chip动态渲染星座列表比RecyclerView更轻量、比RadioGroup更可控在res/layout/fragment_daily_horoscope.xml中定义可滚动的星座选择区ScrollView android:layout_widthmatch_parent android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent com.google.android.material.chip.ChipGroup android:idid/chipGroupZodiac android:layout_widthmatch_parent android:layout_heightwrap_content android:padding16dp app:singleSelectiontrue app:selectionRequiredtrue !-- 动态添加Chip的位置此处留空 -- /com.google.android.material.chip.ChipGroup /ScrollView关键点app:singleSelectiontrue确保单选app:selectionRequiredtrue强制必须选一个避免用户不点直接看运势。3.2 在DailyHoroscopeFragment中动态生成12个星座ChipDailyHoroscopeFragment.kt中用for循环创建Chip并设置点击监听// app/src/main/java/com/example/stellarhoroscope/DailyHoroscopeFragment.kt class DailyHoroscopeFragment : Fragment() { private var _binding: FragmentDailyHoroscopeBinding? null private val binding get() _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding FragmentDailyHoroscopeBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initZodiacChips() } private fun initZodiacChips() { val zodiacs listOf( 白羊座, 金牛座, 双子座, 巨蟹座, 狮子座, 处女座, 天秤座, 天蝎座, 射手座, 摩羯座, 水瓶座, 双鱼座 ) binding.chipGroupZodiac.removeAllViews() // 清空旧Chip避免重复添加 zodiacs.forEachIndexed { index, zodiacName - val chip MaterialChip(requireContext()).apply { text zodiacName isCheckable true isCheckedIconVisible true setTextColor(ContextCompat.getColor(context!!, R.color.text_primary)) layoutParams LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, 0, 12, 12) // 右下边距 } // 设置选中态背景色Material3标准 setChipBackgroundColorResource(R.color.selector_chip_background) setChipStrokeColorResource(R.color.selector_chip_stroke) setChipStrokeWidthResources(R.dimen.chip_stroke_width) } // 为每个Chip绑定唯一ID用于后续识别 chip.id View.generateViewId() binding.chipGroupZodiac.addView(chip) // 设置点击监听记录当前选中的星座 chip.setOnClickListener { val selectedChip binding.chipGroupZodiac.findViewByIdMaterialChip(chip.id) selectedChip?.isChecked true // 强制设为选中 updateHoroscopeDisplay(zodiacName) } } // 默认选中第一个星座 if (binding.chipGroupZodiac.childCount 0) { (binding.chipGroupZodiac.getChildAt(0) as MaterialChip).isChecked true updateHoroscopeDisplay(zodiacs[0]) } } private fun updateHoroscopeDisplay(zodiac: String) { // 此处将触发网络请求或本地JSON读取暂用Toast模拟 Toast.makeText(context, 正在加载$zodiac今日运势..., Toast.LENGTH_SHORT).show() } }核心参数说明setChipBackgroundColorResource(R.color.selector_chip_background)指向res/values/colors.xml中的selector定义未选中/选中/禁用状态色setChipStrokeWidthResources(R.dimen.chip_stroke_width)在res/values/dimens.xml中定义dimen namechip_stroke_width1dp/dimen避免硬编码View.generateViewId()为每个Chip生成唯一ID防止findViewById冲突老式写法用R.id.xxx会报错。3.3 定义Chip状态选择器解决“选中后颜色不变”这个血泪坑res/values/colors.xml中添加!-- res/values/colors.xml -- color namechip_background_unchecked#F5F5F5/color color namechip_background_checked#6750A4/color color namechip_stroke_unchecked#E0E0E0/color color namechip_stroke_checked#6750A4/colorres/drawable/selector_chip_background.xml!-- res/drawable/selector_chip_background.xml -- selector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_checkedtrue android:colorcolor/chip_background_checked / item android:colorcolor/chip_background_unchecked / /selectorres/drawable/selector_chip_stroke.xml!-- res/drawable/selector_chip_stroke.xml -- selector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_checkedtrue android:colorcolor/chip_stroke_checked / item android:colorcolor/chip_stroke_unchecked / /selector注意Material Chip的strokeColor和backgroundColor必须用selector直接写#6750A4会导致选中态失效——这是Android Studio 2023.x版本Material库的已知行为官方文档未明说但Stack Overflow上超2000个提问指向此坑。4. 加载本地星座运势数据用Gson解析assets/json目录下的12个JSON文件绕过网络请求——这才是小白真正能跑通的第一步4.1 准备本地JSON数据按星座名命名结构统一在app/src/main/assets/json/下创建12个文件aries.json,taurus.json,gemini.json...英文名避免中文路径问题。每个文件格式如下{ date: 2024-06-15, title: 白羊座今日运势, content: 事业运上升适合推进新项目。感情方面需注意沟通方式避免急躁。, lucky_color: 红色, lucky_number: 7, summary: 整体运势★★★★☆ }为什么不用SQLite或Room小白阶段数据库引入事务、Migration、Entity定义等概念会指数级增加认知负荷运势数据每日更新一次JSON文件可由运营人员直接替换无需发版assets目录读取快于SD卡且无需申请存储权限。4.2 编写JsonLoader工具类用Gson解析JSON处理空值和字段缺失新建JsonLoader.kt// app/src/main/java/com/example/stellarhoroscope/JsonLoader.kt object JsonLoader { private val gson Gson() /** * 从assets/json目录加载指定星座的运势JSON * param context Application Context避免Fragment中传this导致内存泄漏 * param zodiacName 星座中文名如白羊座 * return HoroscopeData对象若失败返回null */ fun loadHoroscopeData(context: Context, zodiacName: String): HoroscopeData? { val fileName getFileNameByZodiac(zodiacName) return try { val inputStream context.assets.open(json/$fileName) val jsonString inputStream.bufferedReader().use { it.readText() } gson.fromJson(jsonString, HoroscopeData::class.java) } catch (e: Exception) { Log.e(JsonLoader, Failed to load $fileName, e) null } } private fun getFileNameByZodiac(zodiacName: String): String { return when (zodiacName) { 白羊座 - aries.json 金牛座 - taurus.json 双子座 - gemini.json 巨蟹座 - cancer.json 狮子座 - leo.json 处女座 - virgo.json 天秤座 - libra.json 天蝎座 - scorpio.json 射手座 - sagittarius.json 摩羯座 - capricorn.json 水瓶座 - aquarius.json 双鱼座 - pisces.json else - aries.json // 默认返回白羊座 } } } // 数据类字段名必须与JSON key完全一致区分大小写 data class HoroscopeData( val date: String , val title: String , val content: String , val lucky_color: String , val lucky_number: Int 0, val summary: String )关键细节gson.fromJson(jsonString, HoroscopeData::class.java)Kotlin中必须用::class.java写HoroscopeData::class会编译失败val lucky_number: Int 0JSON中若缺失lucky_number字段Gson会赋默认值0避免NullPointerExceptioncontext.assets.open()绝对路径是json/aries.json不是assets/json/aries.json——assets是根目录。4.3 在Fragment中调用JsonLoader并更新UI用LiveData避免主线程阻塞修改DailyHoroscopeFragment.kt中的updateHoroscopeDisplay方法private fun updateHoroscopeDisplay(zodiac: String) { // 显示加载中状态 binding.progressBar.visibility View.VISIBLE binding.tvHoroscopeTitle.text binding.tvHoroscopeContent.text 加载中... // 在IO线程解析JSON lifecycleScope.launch { withContext(Dispatchers.IO) { JsonLoader.loadHoroscopeData(requireContext(), zodiac) }?.let { horoscope - // 切回主线程更新UI withContext(Dispatchers.Main) { binding.progressBar.visibility View.GONE binding.tvHoroscopeTitle.text horoscope.title binding.tvHoroscopeContent.text horoscope.content binding.tvLuckyColor.text 幸运色${horoscope.lucky_color} binding.tvLuckyNumber.text 幸运数字${horoscope.lucky_number} binding.tvSummary.text horoscope.summary } } ?: run { withContext(Dispatchers.Main) { binding.progressBar.visibility View.GONE binding.tvHoroscopeTitle.text 数据加载失败 binding.tvHoroscopeContent.text 请检查assets/json目录文件 } } } }为什么用lifecycleScope而不是GlobalScopeGlobalScope.launch会导致Fragment销毁后协程仍在执行更新已不存在的View引发IllegalStateExceptionlifecycleScope自动绑定Fragment生命周期onDestroyView时自动取消协程withContext(Dispatchers.IO)明确指定IO线程避免在主线程解析大JSON导致ANR。5. 避坑指南12个让小白崩溃的真实问题现象→原因→解决每一条都来自我Debug三天的日志截图5.1 现象点击Chip无反应Logcat显示“No virtual method setOnClickListener”原因在build.gradle中误用了旧版Material库com.google.android.material:material:1.4.0该版本MaterialChip没有setOnClickListener方法必须用setOnCheckedChangeListener。解决升级Material库至1.10.0并在Chip创建后改用setOnCheckedChangeListenerchip.setOnCheckedChangeListener { _, isChecked - if (isChecked) { updateHoroscopeDisplay(zodiacName) } }5.2 现象ViewPager2滑动时底部Tab不联动或Tab点击后ViewPager不跳转原因ViewPager2.registerOnPageChangeCallback和BottomNavigationView.setOnItemSelectedListener未正确配对且binding.viewPager.currentItem position在onCreateView中执行过早View未attach。解决将binding.viewPager.currentItem 0移到onViewCreated中并确保setOnItemSelectedListener返回truebinding.bottomNav.setOnItemSelectedListener { item - when (item.itemId) { R.id.action_daily - binding.viewPager.currentItem 0 R.id.action_weekly - binding.viewPager.currentItem 1 R.id.action_monthly - binding.viewPager.currentItem 2 } true // 必须返回true否则事件不消费 }5.3 现象深色模式下Chip文字消失背景色与文字色相同原因setTextColor()硬编码了颜色值未使用?attr/colorOnSurface主题属性。解决在styles.xml中定义主题色Chip创建时用textColor ColorStateList.valueOf(ContextCompat.getColor(...))替换为chip.setTextColor( ColorStateList.valueOf( ContextCompat.getColor(context!!, androidx.core.R.attr.colorOnSurface) ) )5.4 现象JSON解析报Expected BEGIN_OBJECT but was STRING原因assets目录下JSON文件BOM头未去除或文件编码非UTF-8。解决用VS Code打开JSON文件 → 右下角点击编码 → 选择Save with Encoding→UTF-8→ 勾选Remove BOM。5.5 现象首次安装App闪退Logcat显示java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/appcompat/R$styleable原因appcompat库版本与Material库版本不匹配如appcompat:1.5.0material:1.10.0。解决统一升级至最新稳定版在build.gradle中添加implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.10.06. 让UI真正美观的三个进阶技巧用MotionLayout做星座切换动画、用Palette提取运势卡片主色、用Preview注解秒测深色模式适配6.1 用MotionLayout实现星座卡片入场动画比Alpha渐变更自然将fragment_daily_horoscope.xml中的ScrollView替换为MotionLayoutandroidx.constraintlayout.motion.widget.MotionLayout android:layout_widthmatch_parent android:layout_height0dp app:layoutDescriptionxml/motion_scene_zodiac app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent com.google.android.material.chip.ChipGroup android:idid/chipGroupZodiac android:layout_widthmatch_parent android:layout_heightwrap_content android:padding16dp app:singleSelectiontrue app:selectionRequiredtrue / /androidx.constraintlayout.motion.widget.MotionLayout创建res/xml/motion_scene_zodiac.xmlMotionScene xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:motionhttp://schemas.android.com/apk/res-auto Transition motion:constraintSetStartid/start motion:constraintSetEndid/end motion:duration300 KeyFrameSet KeyAttribute motion:framePosition0 motion:targetid/chipGroupZodiac android:alpha0.0 / KeyAttribute motion:framePosition100 motion:targetid/chipGroupZodiac android:alpha1.0 / /KeyFrameSet /Transition ConstraintSet android:idid/start Constraint android:idid/chipGroupZodiac motion:layout_constraintTop_toTopOfparent / /ConstraintSet ConstraintSet android:idid/end Constraint android:idid/chipGroupZodiac motion:layout_constraintTop_toTopOfparent / /ConstraintSet /MotionScene效果每次切换星座Tab时ChipGroup从透明渐显比硬切更柔和。MotionLayout优势在于动画逻辑与UI分离无需写Java/Kotlin代码且支持手势拖拽中断。6.2 用Palette API从运势图片提取主色动态设置Toolbar背景假设每个星座JSON中增加image_url字段下载图片后用Palette提取色值// 在loadHoroscopeData成功后调用 fun extractPrimaryColorFromImage(bitmap: Bitmap, callback: (Int) - Unit) { Palette.from(bitmap).generate { palette - val vibrantSwatch palette?.vibrantSwatch ?: palette?.lightVibrantSwatch callback(vibrantSwatch?.rgb ?: Color.parseColor(#6750A4)) } }然后设置Toolbarbinding.toolbar.setBackgroundColor(primaryColor) binding.toolbar.setTitleTextColor( if (ColorUtils.calculateLuminance(primaryColor) 0.5) Color.BLACK else Color.WHITE )为什么不用第三方库Palette是AndroidX官方库无额外包体积ColorUtils.calculateLuminance自动判断文字颜色避免深色背景配黑字的可读性灾难。6.3 用Preview注解在Android Studio中实时预览深色模式在DailyHoroscopeFragment.kt上方添加Preview(showBackground true, uiMode Configuration.UI_MODE_NIGHT_YES) Preview(showBackground true, uiMode Configuration.UI_MODE_NIGHT_NO) Composable fun DailyHoroscopePreview() { StellarHoroscopeTheme { DailyHoroscopeScreen() } }前提条件项目已启用Jetpack Compose即使主界面用XMLPreview也需Compose支持StellarHoroscopeTheme是自定义Theme继承MaterialThemeDailyHoroscopeScreen()是用Compose重写的UI组件可逐步迁移。我的习惯是每写一个XML布局必配两个Preview——一个亮色、一个暗色。这比反复切换系统设置快10倍且能发现?attr/colorSurface在深色模式下是否真的生效。去年重构一个天气App靠这个习惯提前发现了7个深色模式下的文字对比度不达标问题避免了上架后被Google Play拒审。希望帮到你。本文还有配套的精品资源点击获取