Android开发 | 碎片Fragment

Android开发之探究Fragment,总结常见问题

一、碎片的简介

碎片Fragment在表现形式上如同活动Activity的姊妹,但它的实际原理又与Activity颇为不同。Fragment扩展自android.app中的Object,而Activity继承于Context,Fragment拥有自己完整的生命周期,但是又依赖于Activity而存活。

碎片最大的特点是它可以作为页面中的一部分控件,而无需占据整个屏幕。因此,多个碎片布局在Activity中就好比一个页面同时存在多个Activity,从效果和作用上而言,碎片更像是一个活动中的子活动,能够彼此独立运行,各自拥有独立空间。打个比方,如果在一个页面中设置多个控件(不使用Fragment),那么就免不了布局的嵌套,而该页面又存活于同一个Activity,那么对各控件(子布局)的操纵将非常复杂。但使用Fragment时,就能让一个活动中存在多个类似活动的控件,彼此拥有独立的生命周期,同时还能逐个针对地进行调整。

Fragment最为直观的例子就是平板设备的左右布局,即左边显示列表,右边显示内容,但在手机上运行时,右边布局又会被隐藏,其会作为一个新活动显示。

/androiddev-charpter4-fragment/mobile.webp
手机的设计方案
/androiddev-charpter4-fragment/pad.webp
平板的双页设计

二、加载碎片

2.1 碎片基本用法

创建碎片的基本布局,同时为其创建一个继承于Fragment的类,重写onCreateView方法(创建布局)。之后便可在其他布局中添加fragment标签,同时指定name属性为上述自定义Fragment类的完整包路径,即可实现基本的碎片定义及创建。

定义左部布局:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_show_right_frag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Show right fragment" />

    <Button
        android:id="@+id/btn_hide_right_frag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="hide right fragment" />
</LinearLayout>

定义右部布局:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00ff00"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="This is a right fragment"
        android:textSize="24sp" />
</LinearLayout>

为布局定义类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// leftFragment.class

public class LeftFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.left_fragment, container, false);
        return view;
    }
}

// rightFragment.class

public class RightFragment extends Fragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }
}

activity_main.xml定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/left_fragment"
        android:name="com.cosyspark.fragmentexample.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout
        android:id="@+id/another_right_fragmentLayout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

2.2 动态加载碎片

动态加载布局主要利用FragmentManager碎片管理器,通过获取当前的碎片管理器,调用beginTransaction()方法,通过replace()方法向容器内添加或替换碎片,传入布局id和碎片实例,最后调用commit()提交事务。

关键代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btn_show_right_frag:
            replaceFragment(new AnotherRightFragment());
            break;
    }
}

private void replaceFragment(Fragment fragment) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    transaction.replace(R.id.another_right_fragmentLayout, fragment);
    transaction.addToBackStack(null);
    transaction.commit();
}

通过在事物提交前添加addToBackStack(null)方法,即可模拟返回栈,此时点击返回会撤销最新加载的碎片。其中,参数用于描述返回栈的状态,可为空。

2.3 碎片与活动间的通信

为了方便碎片和活动间进行通信,FragmentManager提供了一个类似findViewById()的方法,用于从布局id获取对应碎片的实例:

1
RightFragment rightFragment = (RightFragment) getFragmentManager().findViewById(R.id.right_fragment);)

上述方法告诉我们可以通过findViewById()方法在活动中获取碎片的实例,那么如何在碎片中获取活动的实例呢?这个更加简单,只需调用getActivity()方法即可:

1
CosySparkActivity activity = (CosySparkActivity) getActivity(); 

三、碎片的生命周期

/androiddev-charpter4-fragment/lifecycler.webp
碎片的生命周期

碎片的生命周期与活动相同,一样也存在的四个状态:运行、暂停、停止、销毁状态。

在调用之前提及的addToBackStack()方法后,碎片会进入停止状态。进入停止状态的碎片将对用户不可见,其内存有可能会被系统回收。

此外,碎片又多了一些附加的回调方法:

  • onAttach() 碎片与活动建立关联时
  • onCreateView() 碎片加载布局时
  • onActivityCreated() 与碎片关联的活动都创建完毕时
  • onDestroyView() 与碎片相关的视图被移除时
  • onDetach() 碎片与活动解除关联时

四、动态加载布局的技巧

4.1 限定符

利用安卓SDK内置的限定符特性,我们可以针对不同屏幕尺寸的设备,为同一个页面创建适合的控件布局,即将各自的.xml创建在layout、layout-small、layout-xlarge、layout-sw600dp(最小宽度限定符)等资源文件夹(/res/)中,且布局标签的id保持一致,系统会根据屏幕尺寸进行选择。

Android中常见的限定符如下:

/androiddev-charpter4-fragment/xdf.webp
安卓SDK内置的限定符

五、问题总结

在写书中的新闻应用时,遇到一些issues,总结一下。

首先是androidx和android.support依赖库的混用问题。

由于安卓9之后,谷歌重整了传统的android.support依赖库,提出了一个新的依赖库——androidx,它综合了诸如RecycleView和fragment等组件库。在项目中如果有传统依赖库和androidx库混用的情况,需在项目文件夹下的gradle.properties中添加:

1
android.enableJetifier=true

其次,对于一个项目中的同一组件,最好同一其库依赖,不要在不同的class文件中导入不同的依赖库依赖,如以下颇具隐患的导入方式:

1
2
3
4
5
6
7
8
9
// left_fragment.class
import android.support.v4.app.fragment
···
···

// myAdapter.class
import androidx.fragment.app.Fragment
···
···

上述导入方式将会导致以下问题(error部分):

/androiddev-charpter4-fragment/error.webp
Error部分

另外,如果项目出现project error(99+),通常是因为更新了gradle,但缓存未清除导致ide索引时未同步更新,进而造成的命名空间、关键字等的识别异常及丢失问题。此时,可以尝试清除项目缓存:

/androiddev-charpter4-fragment/solution.webp
解决方案

最后,记录一下目前无法解决的问题:fragment创建时跳过了加载布局的回调方法(未知缘由),导致激发创建该fragment的触发器时,app会闪退。目前debug得到的部分原因是,fragment的onCreateView()回调方法会对私有成员对象view进行实例化,但是在debug时分析得知该回调方法并未被调用,导致view自始至终为null,进一步致使调用refresh()方法时,操作了空对象view,触发异常,app闪退。至于为什么onCreateView()回调方法未被调用,未找到原因,等某天找到问题所在时,笔者将发文总结。

/androiddev-charpter4-fragment/qa1.webp
未解决的疑问
/androiddev-charpter4-fragment/qa2.webp
未解决的疑问
/androiddev-charpter4-fragment/qa3.webp
未解决的疑问
给作者倒杯卡布奇诺 ~
Albresky 支付宝支付宝
Albresky 微信微信