Fragment的最佳实践:一个简易版的新闻应用-填坑记录

今天照着书上把代码写了一下,本以为一样的东西绝对能跑,可以还是想简单了。 首先我是把书籍自带的源码用android studio打开,想先跑一下。 结果发现各种报错,应该是studio版本和代码中的不匹配,代码有点老了。

然后用studio重新创建个项目。把app/src/main下的代码都复制到新项目中。 结果发现还是各种报错。 这里记录一下过程中的问题,以防忘记。

一、 首先是kotlin-android-extensions 过期的问题

kotlin-android-extensions 过期以后,findFragmentById()用什么替代?

kotlin-android-extensions 被废弃后,官方推荐使用 View Binding 作为替代方案来安全地访问视图资源,包括 Fragment 的查找。对于 findFragmentById() 本身,它本质上是 FragmentManager 的 API,并非 kotlin-android-extensions 提供的功能,因此仍可继续使用,但建议结合 View Binding 来获取布局中的 Fragment 容器 ID,以获得更安全的类型检查。

具体替代方案

  1. 启用 View Binding 在模块的 build.gradle 中启用 View Binding:
arduino 复制代码
android {
    ...
    buildFeatures {
        viewBinding true
    }
}
  1. 通过 View Binding 获取容器 ID,结合 findFragmentById() View Binding 会为每个布局文件生成对应的绑定类(如 ActivityMainBinding),通过绑定类可以安全获取视图 ID,避免直接使用硬编码的 R.id.xxx,再结合 FragmentManagerfindFragmentById() 查找 Fragment。

示例代码

1. 在 Activity 中查找 Fragment

假设布局文件 activity_main.xml 中有一个 Fragment 容器:

xml 复制代码
<!-- activity_main.xml -->
<FrameLayout
    android:id="@+id/fragmentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

通过 View Binding 查找 Fragment:

kotlin 复制代码
import com.example.yourpackage.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    // 声明ViewBinding变量
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 初始化ViewBinding
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 通过ViewBinding获取容器ID,结合findFragmentById()查找Fragment
        val myFragment = supportFragmentManager.findFragmentById(binding.fragmentContainer.id) as? MyFragment
        
        // 如果Fragment未实例化,可进行添加
        if (myFragment == null) {
            supportFragmentManager.beginTransaction()
                .add(binding.fragmentContainer.id, MyFragment())
                .commit()
        }
    }
}

这里supportFragmentManager 是 AndroidX 库中 FragmentActivity (及其子类,如 AppCompatActivity)提供的一个属性,用于获取管理当前 Activity 中所有 Fragment 的 FragmentManager 实例

核心作用:

它是 Activity 与 Fragment 之间的 "桥梁",主要负责:

  1. 管理 Activity 中添加的所有 Fragment 的生命周期(创建、销毁、切换等);
  2. 处理 Fragment 事务(如添加 add()、替换 replace()、移除 remove() 等操作);
  3. 查找已添加到 Activity 中的 Fragment(通过 findFragmentById()findFragmentByTag())。

为什么叫 "support"?

这个命名源于早期的 Android 支持库(Support Library)。在 Android 系统早期,原生系统对 Fragment 的支持不完善,Google 推出了支持库来兼容低版本系统。后来支持库升级为 AndroidX (现在的官方推荐库),supportFragmentManager 也随之成为 AndroidX 中管理 Fragment 的标准接口。

与其他 FragmentManager 的区别:

  • supportFragmentManager:属于 Activity,用于管理 Activity 直接包含的 Fragment。
  • childFragmentManager:属于 Fragment,用于管理当前 Fragment 内部包含的 "子 Fragment"(Fragment 嵌套场景)。
2. 在 Fragment 中查找子 Fragment

如果需要在一个 Fragment 中查找其内部的子 Fragment,应使用 childFragmentManager

kotlin 复制代码
class ParentFragment : Fragment() {
    private lateinit var binding: FragmentParentBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentParentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 查找子Fragment(使用childFragmentManager)
        val childFragment = childFragmentManager.findFragmentById(binding.childFragmentContainer.id) as? ChildFragment
        
        if (childFragment == null) {
            childFragmentManager.beginTransaction()
                .add(binding.childFragmentContainer.id, ChildFragment())
                .commit()
        }
    }
}

优势说明

  • 类型安全:View Binding 生成的绑定类会对视图 ID 进行类型检查,避免因 ID 错误导致的运行时异常。
  • 空安全:绑定类会自动处理视图的空值情况,无需手动判空。
  • 替代 synthetic 扩展 :完全替代了 kotlin-android-extensions 的 synthetic 视图访问方式,是官方推荐的长期解决方案

二、newsContentFrag as NewsContentFragment 报错Unresolved reference: newsContentFrag

kotlin 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_news_content)
        val title = intent.getStringExtra("news_title") // 获取传入的新闻标题
        val content = intent.getStringExtra("news_content") // 获取传入的新闻内容
        if (title != null && content != null) {
            val fragment = newsContentFrag as NewsContentFragment
            fragment.refresh(title, content) //刷新NewsContentFragment界面
        }
    }

这里直接写 val fragment = newsContentFrag as NewsContentFragment 是kotlin-android-extensions的写法。这里改成

ini 复制代码
val fragment = supportFragmentManager
    .findFragmentById(R.id.news_content_frag) as NewsContentFragment

然后还把activity_news_content.xml中fragment的id从NewsContentFrag改成news_content_frag

三、同名但不同格式的资源文件导致的报错

swift 复制代码
报错AGPBI: {"kind":"error","text":"Duplicate resources","sources":[{"file":{"description":"mipmap-hdpi-v4/ic_launcher","path":"D:\\androidhome\\FragmentDemo\\app\\src\\main\\res\\mipmap-hdpi\\ic_launcher.png"}},{"file":{"description":"mipmap-hdpi-v4/ic_launcher","path":"D:\\androidhome\\FragmentDemo\\app\\src\\main\\res\\mipmap-hdpi\\ic_launcher.webp"}}],"tool":"Resource and asset merger"}

这个错误的原因是:在同一个资源目录(mipmap-hdpi)下存在同名但不同格式的资源文件ic_launcher.pngic_launcher.webp),导致 Android 资源合并工具无法区分,从而报 "重复资源" 错误。

解决步骤:

定位重复文件 打开项目目录 D:\androidhome\FragmentDemo\app\src\main\res\mipmap-hdpi,会看到两个文件:

markdown 复制代码
-   `ic_launcher.png`
-   `ic_launcher.webp`

然后同名的删掉一个就行,我删了ic_launcher.webp。 其他文件找了一下也把同名的删掉了

四、依赖库版本与编译 SDK 版本不兼容

vbnet 复制代码
报错 xecution failed for task ':app:checkDebugAarMetadata'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.CheckAarMetadataWorkAction
   > An issue was found when checking AAR metadata:
     
       1.  Dependency 'androidx.recyclerview:recyclerview:1.4.0' requires libraries and applications that
           depend on it to compile against version 35 or later of the
           Android APIs.
     
           :app is currently compiled against android-34.
     
           Also, the maximum recommended compile SDK version for Android Gradle
           plugin 8.5.2 is 34.

这个错误的核心原因是 依赖库版本与编译 SDK 版本不兼容androidx.recyclerview:recyclerview:1.4.0 要求项目编译 SDK 版本至少为 35 ,但你的项目当前使用的是 34,且当前 Android Gradle 插件(AGP 8.5.2)最高推荐的编译 SDK 版本也是 34,导致冲突。

解决方案(二选一)

方案 1:降低 RecyclerView 版本以适配当前编译 SDK 34(推荐,改动小)

recyclerview:1.4.0 是较新版本,对编译 SDK 要求较高。可以降级到兼容 SDK 34 的版本(如 1.3.2),无需修改编译 SDK 版本。

修改 模块级 build.gradleapp/build.gradle)中的依赖:

arduino 复制代码
dependencies {
    // 将原来的 1.4.0 改为 1.3.2
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    // 其他依赖...
}

我实际改的是gradle/libs.versions.toml 中的

ini 复制代码
recyclerview = "1.3.2"

这个版本是这样找的

build.gradle.kts中有

scss 复制代码
dependencies {
    implementation(libs.androidx.recyclerview)
}

这个对应 libs.versions.toml中的

csharp 复制代码
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }

这里version.ref = "recyclerview" }就是对应recyclerview = "1.3.2"

四、新版 Android Gradle 插件(AGP 7.0+)已废弃通过 AndroidManifest.xmlpackage 属性设置应用命名空间

ini 复制代码
Incorrect package="com.example.fragmentbestpractice" found in source AndroidManifest.xml: D:\androidhome\FragmentDemo\app\src\main\AndroidManifest.xml.
Setting the namespace via the package attribute in the source AndroidManifest.xml is no longer supported.
Recommendation: remove package="com.example.fragmentbestpractice" from the source AndroidManifest.xml: D:\androidhome\FragmentDemo\app\src\main\AndroidManifest.xml.
报错

这个错误的核心原因是 新版 Android Gradle 插件(AGP 7.0+)已废弃通过 AndroidManifest.xmlpackage 属性设置应用命名空间 ,转而要求在 build.gradle 中统一配置 namespace。Manifest 中保留的 package 属性与 Gradle 配置冲突,导致编译失败。

解决

步骤 1:删除 AndroidManifest.xml 中的 package 属性

打开报错的 Manifest 文件(路径:D:\androidhome\FragmentDemo\app\src\main\AndroidManifest.xml),找到根标签 <manifest> 中的 package 属性并删除:

xml 复制代码
<!-- 修改前:<manifest> 标签包含 package 属性(错误根源) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.fragmentbestpractice"> <!-- 删掉这行的 package 属性 -->

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        ...>
        <!-- 组件声明(Activity/Fragment等)不变 -->
    </application>

</manifest>

<!-- 修改后:<manifest> 标签无 package 属性 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 其余内容完全不变 -->
    <application ...>...</application>
</manifest>
步骤 2:确认 build.gradle 中已配置 namespace(关键,否则会丢失包名)

新版 AGP 要求在 模块级 build.gradleapp/build.gradle)的 android 闭包中通过 namespace 指定应用包名,且值必须与之前 Manifest 中的 package 一致(即 com.example.fragmentbestpractice)。

检查并补充配置:

arduino 复制代码
android {
    namespace "com.example.fragmentbestpractice" // 必须添加这行,值与原package一致
    compileSdk 34 // 你的编译SDK版本(如34/35)
    defaultConfig {
        applicationId "com.example.fragmentbestpractice" // (可选但建议保留)与namespace一致
        minSdk 21 // 你的最小SDK版本
        targetSdk 34 // 你的目标SDK版本
        ...
    }
    ...
}

原理说明:为什么 Manifest 的 package 属性被废弃?

在 AGP 7.0 之前,应用的 "包名" 有两个来源:

  1. AndroidManifest.xmlpackage 属性:控制资源引用(如 R.id.xxx)和代码命名空间。
  2. build.gradleapplicationId:控制应用在设备上的唯一标识。

这种 "双来源" 容易导致不一致(比如改了一个没改另一个),因此新版 AGP 统一为 namespace 控制代码 / 资源命名空间,applicationId 控制设备标识 ,并废弃了 Manifest 的 package 属性,避免冲突。

常见问题排查

  1. 同步后提示 "找不到 R 类" :原因是 namespace 配置错误或未配置,导致资源生成路径异常。重新检查 build.gradlenamespace 是否与原 package 完全一致(大小写、拼写都不能错)。
  2. 组件(如 Activity)注册报错 "未声明" :无需修改 Manifest 中的组件声明(如 <activity android:name=".MainActivity"/>),namespace 会自动作为前缀,等同于原 package 的作用。
  3. 依赖库引用报错 :确保 build.gradlenamespace 与依赖库要求的包名无冲突,同步后通常会自动修复。

五、在同一个布局文件(activity_main.xml)的不同变体(如不同屏幕尺寸、横竖屏等配置)中,根元素的 id 不一致

rust 复制代码
Execution failed for task ':app:dataBindingGenBaseClassesDebug'.
> Configurations for activity_main.xml must agree on the root element's ID.

这个错误的核心原因是:在同一个布局文件(activity_main.xml)的不同变体(如不同屏幕尺寸、横竖屏等配置)中,根元素的 id 不一致,导致 Data Binding 生成基类时无法统一处理。

具体分析

Android 允许为不同场景(如横竖屏、不同屏幕尺寸)创建布局变体(例如:

  • res/layout/activity_main.xml(默认)
  • res/layout-sw600dp/activity_main.xml(平板)

这些布局文件虽然文件名相同,但如果它们的根元素 id 不同 (例如一个根元素是 android:id="@+id/main_root",另一个是 android:id="@+id/root_layout"),Data Binding 就会报错,因为它需要统一的根元素 ID 来生成绑定类。

这里其实是因为res/layout-sw600dp/activity_main.xml的linearLayout标签有id

ini 复制代码
android:id="@+id/news_title_layout" 

res/layout/activity_main.xml 里没有

六、'onActivityCreated(Bundle?): Unit' is deprecated. Deprecated in Java

onActivityCreated() 是 Fragment 中的一个生命周期方法,主要用于在 Activity 的 onCreate() 完成后执行初始化操作。但随着 Android 开发规范的演进,这个方法因可能导致 Fragment 与 Activity 过度耦合而被 废弃(deprecated)

替代方案:使用 onViewCreated()

官方推荐将原本在 onActivityCreated() 中的逻辑迁移到 onViewCreated() 中。onViewCreated() 在 Fragment 的视图(View)创建完成后立即调用,此时可以安全地访问视图控件,且无需依赖 Activity 的生命周期状态。

代码迁移示例

旧代码(使用 onActivityCreated(),已废弃):
kotlin 复制代码
override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    // 初始化视图(如设置RecyclerView适配器、绑定数据等)
    val recyclerView = view?.findViewById<RecyclerView>(R.id.recycler_view)
    recyclerView?.adapter = MyAdapter(dataList)
}
新代码(使用 onViewCreated(),推荐):
kotlin 复制代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 直接使用参数中的 view 访问控件,或通过 ViewBinding 获取
    val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
    recyclerView.adapter = MyAdapter(dataList)

    // 如果使用 ViewBinding:
    // binding.recyclerView.adapter = MyAdapter(dataList)
}

七、'getter for adapterPosition: Int' is deprecated. Deprecated in Java

在 RecyclerView 的 ViewHolder 中,adapterPosition属性已被废弃,官方推荐使用 bindingAdapterPosition 替代。这一变更的核心原因是adapterPosition在某些场景(如数据动态更新时)可能返回不准确的位置,而bindingAdapterPosition能更可靠地反映当前 item 在绑定适配器中的位置。

新代码(使用 bindingAdapterPosition,推荐):
scss 复制代码
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    init {
        itemView.setOnClickListener {
            // 替换为 bindingAdapterPosition
            val position = bindingAdapterPosition
            if (position != RecyclerView.NO_POSITION) {
                // 处理点击逻辑(位置更可靠)
                onItemClick(position)
            }
        }
    }
}

关键说明

  1. 为什么废弃 adapterPositionadapterPosition 依赖于 RecyclerView 的布局状态,在数据发生变化(如调用notifyDataSetChanged())但尚未完成布局更新时,可能返回过时的位置信息,导致索引错误。

  2. bindingAdapterPosition 的优势:它直接关联到适配器的绑定状态,即使数据更新后,也能准确反映当前 item 在适配器中的实际位置,减少因位置错误导致的崩溃。

  3. 额外注意事项

    • 无论使用哪种方式,都建议判断 position != RecyclerView.NO_POSITION,避免 item 已被移除时的异常。
    • 在 Kotlin 中,可直接使用属性访问(bindingAdapterPosition),无需调用getBindingAdapterPosition()(Java 中需用 getter 方法)。
  4. RecyclerView.NO_POSITION 是什么? 它是 RecyclerView 中定义的一个常量,值为 -1,表示 "无效的位置"。当一个 item 被删除、未显示在屏幕上,或因数据变化导致位置失效时,获取到的位置会是这个值。

  5. 为什么需要这个判断? 在 RecyclerView 中,item 的位置可能因为以下情况变得无效:

    • 该 item 已经被从数据源中删除(但尚未从屏幕上移除);
    • 数据发生了变化(如调用了notifyDataSetChanged()),但布局还未完成更新;
    • 用户快速点击时,item 可能已经被回收或移除。

    此时如果直接使用 position(可能为 -1)去操作数据源(如获取数据、更新 UI 等),会导致类似 "数组索引越界" 的错误。

八、NewsTitleFragment 中使用private lateinit var activityMainBinding: ActivityMainBinding 的报错

kotlin 复制代码
报错at com.example.fragmentdemo.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:53)
at com.example.fragmentdemo.NewsTitleFragment.onViewCreated(NewsTitleFragment.kt:31)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128) 代码是:class NewsTitleFragment : Fragment() {    

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 直接使用参数中的 view 访问控件,或通过 ViewBinding 获取
        isTwoPane = activity?.findViewById<View>(R.id.news_content_layout) != null
        val layoutManager = LinearLayoutManager(activity)
        newsTitleFragBinding = NewsTitleFragBinding.inflate(layoutInflater)
        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        val newsTitleRecyclerView = newsTitleFragBinding.newsTitleRecyclerView
        newsTitleRecyclerView.layoutManager = layoutManager
        val adapter = NewsAdapter(getNews())
        newsTitleRecyclerView.adapter = adapter
    }

问题出在 Fragment 中不恰当地初始化了 Activity 的 ViewBinding。在 Fragment 中直接创建 ActivityMainBinding 实例是不合适的,这违背了组件的职责划分。

正确的做法是 Fragment 只负责管理自己的视图绑定,而不是直接操作 Activity 的绑定。

修改后:

kotlin 复制代码
package com.example.fragmentdemo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.fragmentdemo.databinding.NewsTitleFragBinding
import java.util.Random

class NewsTitleFragment : Fragment() {
    private lateinit var newsTitleFragBinding: NewsTitleFragBinding

    //    private lateinit var activityMainBinding: ActivityMainBinding
    private var isTwoPane = false

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.news_title_frag, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 直接使用参数中的 view 访问控件,或通过 ViewBinding 获取
        isTwoPane = activity?.findViewById<View>(R.id.news_content_layout) != null
        val layoutManager = LinearLayoutManager(activity)
        newsTitleFragBinding = NewsTitleFragBinding.bind(view)
//        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        val newsTitleRecyclerView = newsTitleFragBinding.newsTitleRecyclerView
        newsTitleRecyclerView.layoutManager = layoutManager
        val adapter = NewsAdapter(getNews())
        newsTitleRecyclerView.adapter = adapter
    }

    private fun getNews(): List<News> {
        val newsList = ArrayList<News>()
        for (i in 1..50) {
            val news =
                News("This is news title $i", getRandomLengthString("This is news content $i. "))
            newsList.add(news)
        }
        return newsList
    }

    private fun getRandomLengthString(str: String): String {
        val n = Random().nextInt(20) + 1
        return str * n
    }

    inner class NewsAdapter(val newsList: List<News>) :
        RecyclerView.Adapter<NewsAdapter.ViewHolder>() {

        inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
            val newsTitle: TextView = view.findViewById(R.id.newsTitle)
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            val view =
                LayoutInflater.from(parent.context).inflate(R.layout.news_item, parent, false)
            val holder = ViewHolder(view)
            holder.itemView.setOnClickListener {
                val news = newsList[holder.bindingAdapterPosition]
                if (isTwoPane) {
                    // 如果是双页模式,则刷新NewsContentFragment中的内容
//                    val fragment = activityMainBinding.newsContentLayout as NewsContentFragment
//                    fragment.refresh(news.title, news.content) //刷新NewsContentFragment界面
                    // 双页模式:更新右侧 Fragment(通过 FragmentManager 替换)
                    val fragment = NewsContentFragment.newInstance(news.title, news.content)
                    parentFragmentManager.beginTransaction()
                        .replace(R.id.news_content_layout, fragment) // 这里用 ID 而非 Binding
                        .commit()
                } else {
                    // 如果是单页模式,则直接启动NewsContentActivity
                    NewsContentActivity.actionStart(parent.context, news.title, news.content);
                }
            }
            return holder
        }

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val news = newsList[position]
            holder.newsTitle.text = news.title
        }

        override fun getItemCount() = newsList.size

    }

}

注意这里在 NewsContentFragment 中添加 newInstance 方法

后面记录一下填坑以后的可运行版本吧

相关推荐
CRMEB定制开发2 小时前
PHP多商户接入阿里云识图找商品
android·阿里云·小程序·php·商城系统·微信商城·crmeb
00后程序员张3 小时前
iOS App 混淆实战,在源码不可用情况下的成品加固与测试流程
android·ios·小程序·https·uni-app·iphone·webview
Jeremy_Lee1234 小时前
MySQL 数据导出及备份方法
android
西西学代码4 小时前
安卓开发---写项目的注意事项
android
come112345 小时前
深入分析JAR和WAR包的区别 (指南七)
android·spring boot·后端
用户095 小时前
停止滥用 Dispatchers.IO:Kotlin 协程调度器的深度陷阱与优化实战
android·面试·kotlin
峥嵘life5 小时前
Android16 adb投屏工具Scrcpy介绍
android·开发语言·python·学习·web安全·adb
遇见你的那天6 小时前
反编译查看源码
android
用户2018792831676 小时前
SIGABRT+GL errors Native Crash 问题分析
android