Android Fragment(碎片)基础使用指南

文章目录

    • [1. 为什么要学 Fragment?](#1. 为什么要学 Fragment?)
    • [2. Fragment 是什么](#2. Fragment 是什么)
    • [3. 写一个 Fragment 的步骤](#3. 写一个 Fragment 的步骤)
    • [4. Fragment 的生命周期](#4. Fragment 的生命周期)
    • [5. Fragment 里用 RecyclerView](#5. Fragment 里用 RecyclerView)
    • [6. Fragment 之间怎么通信?](#6. Fragment 之间怎么通信?)
      • [同一屏上的两个 Fragment:直接找](#同一屏上的两个 Fragment:直接找)
      • [不在同一屏:通过 Activity 中转](#不在同一屏:通过 Activity 中转)
    • [7. 把 Adapter 写成内部类](#7. 把 Adapter 写成内部类)
    • [8. 总结](#8. 总结)

1. 为什么要学 Fragment?

刚学 Android 的时候,我们都会写 Activity。一个页面就是一个 Activity,跳来跳去,挺好理解。

但问题来了------你去看看平板上的新闻 APP。屏幕左边是标题列表,右边是内容详情,两个区域并排显示、各玩各的。如果还按"一个页面 = 一个 Activity"的思路写,你得分两个 Activity 还是合一个?怎么复用手机上的代码?

Fragment 就是来解决这类问题的。把界面上可以独立运作的一块区域抽成一个 Fragment,想塞进哪个 Activity 就塞进哪个。

拿新闻应用来说:

复制代码
手机(小屏):                     平板(大屏):
┌───────────────┐               ┌─────────┬───────────┐
│               │               │ 标题    │ 内容      │
│  标题列表     │  点击一条 →    │ Fragment│ Fragment  │
│  Fragment     │  跳新页面      │ 在同一个│ 在同一个   │
│  (占满全屏)   │               │Activity │ Activity  │
└───────────────┘               └─────────┴───────────┘

同一套 Fragment 代码,手机上分两屏展示,平板上并排展示。 这就是 Fragment 最核心的价值。

下面结合实际代码,一步一步说清楚 Fragment 怎么写、怎么用。


2. Fragment 是什么

Fragment 说白了就是一个"可以塞进 Activity 的 UI 组件"。它有自己的布局文件、自己的生命周期、自己的事件处理,但不能独立存在,必须挂在某个 Activity 里面。

对比一下 Activity 和 Fragment:

Activity Fragment
独立运行? 不能,必须依附 Activity
需要在 AndroidManifest 注册? 需要 不需要
怎么拿到 Context? this 就是 getActivity()
生命周期 自己的 跟着宿主 Activity 走
启动方式 startActivity() FragmentTransaction

AndroidManifest.xml 里就能看出来------只注册了 Activity,Fragment 根本不用管:

xml 复制代码
<application ...>
    <activity android:name=".MainActivity" android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <activity android:name=".NewsContentActivity" android:exported="false" />
</application>

3. 写一个 Fragment 的步骤

最少三步:布局文件 → Fragment 类 → 放进 Activity。来看最简单的例子。

第一步,写布局文件。 和 Activity 的布局完全一样,没有任何特殊标签:

xml 复制代码
<!-- news_content_frag.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/visibility_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="invisible">
        <TextView
            android:id="@+id/news_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textSize="20sp" />
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />
        <TextView
            android:id="@+id/news_content"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="15dp"
            android:textSize="18sp" />
    </LinearLayout>
</RelativeLayout>

内容区域初始设为 invisible,等用户点击标题后再显示,避免一开始就显示一片空白。

第二步,写 Fragment 类。 继承 Fragment,重写 onCreateView()

java 复制代码
public class NewsContentFragment extends Fragment {
    private View view;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.news_content_frag, container, false);
        return view;
    }

    public void refresh(String newsTitle, String newsContent) {
        View visibilityLayout = view.findViewById(R.id.visibility_layout);
        visibilityLayout.setVisibility(View.VISIBLE);
        TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
        TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
        newsTitleText.setText(newsTitle);
        newsContentText.setText(newsContent);
    }
}

注意几个容易踩坑的地方:

  • inflate() 的第三个参数必须传 false,否则布局会被重复添加导致异常
  • findViewById() 得写在 view. 后面,不能直接调用,因为 Fragment 不是 Context
  • view 要存成成员变量,后续 refresh() 里还要用

第三步,把 Fragment 放进 Activity。 有两种方式。

方式一,在 Activity 的 XML 里直接写 <fragment> 标签:

xml 复制代码
<!-- activity_main.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.example.fragmentbestpractice.NewsTitleFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

android:name 填 Fragment 的完整类名。Activity 里啥都不用干,setContentView() 之后 Fragment 自动加载好了。

方式二,在代码里动态添加:

java 复制代码
getSupportFragmentManager()
    .beginTransaction()
    .add(R.id.fragment_container, new NewsTitleFragment())
    .commit();

两种方式怎么选?布局不会变就用静态的,简单省事;需要运行时动态切换(比如底部导航栏切 Tab)就用动态的。


4. Fragment 的生命周期

Fragment 有自己的一套生命周期,但它和宿主 Activity 的生命周期是绑在一起的

复制代码
Activity                     Fragment
 onCreate()      →        onAttach()
                          onCreate()
                          onCreateView()   ← 加载布局的地方
                  →        onActivityCreated()  ← Activity 的 View 就绪了
 onStart()       →        onStart()
 onResume()      →        onResume()       ← 界面可见,可交互
 onPause()       →        onPause()
 onStop()        →        onStop()
 onDestroy()     →        onDestroyView()
                          onDestroy()
                          onDetach()

实际开发中,最常用的就两个:onCreateView()onActivityCreated()

onCreateView() 用来加载布局,相当于 Activity 的 setContentView()

onActivityCreated() 是一个很特殊的节点------此时宿主 Activity 的 onCreate() 已经执行完了,布局里的所有控件都已经可以 findViewById 了。 所以如果要访问 Activity 布局里的控件,必须放在这里。例如判断当前是手机还是平板:

java 复制代码
public class NewsTitleFragment extends Fragment {
    private boolean isTwoPane;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout) != null) {
            isTwoPane = true;  // 平板双页模式
        } else {
            isTwoPane = false; // 手机单页模式
        }
    }
}

这里的设计思路比硬编码判断 isTablet 高明:不去猜设备类型,而是去查当前布局里有没有某个控件。 有就是平板布局,没有就是手机布局。这种方式不管设备怎么变,代码都不用改。


5. Fragment 里用 RecyclerView

Fragment 里用 RecyclerView 和 Activity 里完全一个套路,唯一的区别是 findViewById() 要写到 view. 后面。

先看列表项的布局,就一个 TextView:

xml 复制代码
<!-- news_item.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:maxLines="1"
    android:ellipsize="end"
    android:textSize="18sp"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp" />

Fragment 的布局里放一个 RecyclerView:

xml 复制代码
<!-- news_title_frag.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/news_title_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

然后 onCreateView() 里初始化:

java 复制代码
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.news_title_frag, container, false);
    RecyclerView newsTitleRecyclerView = (RecyclerView) view.findViewById(
            R.id.news_title_recycler_view);
    LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
    newsTitleRecyclerView.setLayoutManager(layoutManager);
    NewsAdapter adapter = new NewsAdapter(getNews());
    newsTitleRecyclerView.setAdapter(adapter);
    return view;
}

三步走:设 LayoutManager → 造 Adapter → 绑上去。

再来造些测试数据:

java 复制代码
private List<News> getNews() {
    List<News> newsList = new ArrayList<>();
    for (int i = 1; i <= 50; i++) {
        News news = new News();
        news.setTitle("This is news title " + i);
        news.setContent(getRandomLengthContent("This is news content " + i + ". "));
        newsList.add(news);
    }
    return newsList;
}

private String getRandomLengthContent(String content) {
    Random random = new Random();
    int length = random.nextInt(20) + 1;
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < length; i++) {
        builder.append(content);
    }
    return builder.toString();
}

数据模型就是一个普通类:

java 复制代码
public class News {
    private String title;
    private String content;
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
}

6. Fragment 之间怎么通信?

同一屏上的两个 Fragment:直接找

平板模式下,标题 Fragment 和内容 Fragment 在同一个 Activity 里。标题 Fragment 想刷新右边的内容,直接用 FragmentManager 捞出目标 Fragment,调它的公开方法:

java 复制代码
NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager()
        .findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());

目标 Fragment 暴露一个 refresh() 方法即可。

不在同一屏:通过 Activity 中转

手机模式下,只有一个 Fragment 在屏幕上,要看详情得跳一个新 Activity。这时候数据通过 Intent 传递:

java 复制代码
// NewsTitleFragment 里点击标题时
NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());

把 Intent 的构造封装成静态方法是个好习惯,调用方不用关心传参细节:

java 复制代码
public class NewsContentActivity extends AppCompatActivity {

    public static void actionStart(Context context, String newsTitle,
                                   String newsContent) {
        Intent intent = new Intent(context, NewsContentActivity.class);
        intent.putExtra("news_title", newsTitle);
        intent.putExtra("news_content", newsContent);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.news_content);

        String newsTitle = getIntent().getStringExtra("news_title");
        String newsContent = getIntent().getStringExtra("news_content");

        NewsContentFragment newsContentFragment = (NewsContentFragment)
                getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
        newsContentFragment.refresh(newsTitle, newsContent);
    }
}

整个数据流在这两条路径:

复制代码
手机:NewsTitleFragment → Intent → NewsContentActivity → findFragmentById → NewsContentFragment.refresh()
平板:NewsTitleFragment → findFragmentById → NewsContentFragment.refresh()
                                 ↑
                          跳过 Activity 中转

7. 把 Adapter 写成内部类

Adapter 写成 Fragment 的内部类挺好用,能直接访问外部类的字段和方法:

java 复制代码
public class NewsTitleFragment extends Fragment {
    private boolean isTwoPane;  // 外部类字段

    class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
        private List<News> mNewsList;

        class ViewHolder extends RecyclerView.ViewHolder {
            TextView newsTitleText;
            public ViewHolder(View view) {
                super(view);
                newsTitleText = (TextView) view.findViewById(R.id.news_title);
            }
        }

        public NewsAdapter(List<News> newsList) {
            mNewsList = newsList;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.news_item, parent, false);
            final ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    if (isTwoPane) {
                        // 平板:直接刷新同屏的内容 Fragment
                        NewsContentFragment newsContentFragment =
                                (NewsContentFragment) getFragmentManager()
                                        .findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refresh(news.getTitle(), news.getContent());
                    } else {
                        // 手机:跳新 Activity
                        NewsContentActivity.actionStart(getActivity(),
                                news.getTitle(), news.getContent());
                    }
                }
            });
            return holder;
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            News news = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }

        @Override
        public int getItemCount() {
            return mNewsList.size();
        }
    }
}

isTwoPanegetFragmentManager() 都是外部类的,内部类直接用,不用传参。点击事件里的判断逻辑也一目了然------是平板就刷右边,是手机就跳页面。


8. 总结

回顾一下,写一个 Fragment 应用大概就这几步:

  1. 写布局 XML(和 Activity 没区别)
  2. 写 Fragment 类,继承 Fragment,重写 onCreateView(),用 inflate() 加载布局
  3. 控件的 findViewById() 前面要加 view.
  4. 放进 Activity:静态用 <fragment> 标签,动态用 FragmentTransaction
  5. 同屏的 Fragment 之间用 findFragmentById() 直接拿实例调方法
  6. 跨屏的通过 Intent → Activity → Fragment 传数据
  7. <fragment> 标签的 android:name 填类全名,不需要在 AndroidManifest 里注册

Fragment 说到底,就是把 UI 切成可复用的独立模块。手机上分页用,平板上拼着用,一套代码服务所有屏幕。理解了这个,基本就能上手了。