Post

【AndroidX】Fragment

1.简介

Fragment表示应用界面中可重复使用的部分,通过将界面划分为独立的块从而实现模块化和可重用性。Fragment定义并管理自己的布局,具有自己的生命周期。但fragment不能独立存在,必须由activity或其他fragment托管。

2.创建fragment

Fragment需要依赖AndroidX Fragment库。在build.gradle文件中添加以下依赖项:

1
implementation "androidx.fragment:fragment:1.8.8"

2.1 创建fragment类

为了创建fragment,需要扩展androidx.fragment.app.Fragment类,并通过超类构造器指定布局资源id。例如:

1
2
3
4
5
6
7
import androidx.fragment.app.Fragment;

public class ExampleFragment extends Fragment {
    public ExampleFragment() {
        super(R.layout.example_fragment);
    }
}

也可以在onCreateView()方法中手动创建布局视图:

1
2
3
4
5
6
public class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_example, container, false);
    }
}

2.2 向activity添加fragment

Fragment必须嵌入AndroidX FragmentActivity中。FragmentActivityAppCompatActivity的超类。因此,如果已经继承了AppCompatActivity,则无需修改activity超类。

为了将fragment添加到activity视图中,可以直接在activity的布局文件中定义fragment;或者在布局文件中定义fragment容器,然后以编程方式添加fragment。强烈建议使用FragmentContainerView(而不是FrameLayout)作为fragment容器。

直接通过XML添加fragment的示例如下:

1
2
3
4
5
6
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.example.ExampleFragment" />

android:name属性指定fragment类名。

为了以编程方式添加fragment,布局应该包含FragmentContainerView作为fragment容器(与上面的示例相同,只是没有android:name属性)。在activity运行时,可以执行fragment事务,如添加、移除或替换fragment。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExampleActivity extends AppCompatActivity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                .setReorderingAllowed(true)
                .add(R.id.fragment_container_view, ExampleFragment.class, null)
                .commit();
        }
    }
}

注意,只有在savedInstanceStatenull时才会创建fragment事务,这是为了确保fragment仅在activity首次创建时添加一次。当activity重新创建时,fragment会自动从savedInstanceState中恢复。

如果fragment需要初始数据,可以通过在FragmentTransaction.add()调用中提供一个Bundle将参数传递到fragment,如下所示:

1
2
3
4
5
6
7
8
9
if (savedInstanceState == null) {
    Bundle bundle = new Bundle();
    bundle.putInt("some_int", 0);

    getSupportFragmentManager().beginTransaction()
        .setReorderingAllowed(true)
        .add(R.id.fragment_container_view, ExampleFragment.class, bundle)
        .commit();
}

然后在fragment中通过调用requireArguments()获取参数:

1
2
3
4
5
6
7
8
9
public class ExampleFragment extends Fragment {
    ...

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        int someInt = requireArguments().getInt("some_int");
        ...
    }
}

注:

  • 也可以单独创建fragment实例,这种情况下需要通过fragment.setArguments(args)设置参数,然后调用add(containerViewId, fragment)
  • 在fragment中也可以通过getArguments()获取参数。requireArguments()与该方法的区别是当参数为null时会抛出异常。

3.保存和恢复状态

各种Android系统操作都会影响fragment的状态。Android框架会自动保存和恢复fragment,而fragment中的数据需要手动保存和恢复。

3.1 视图状态

视图负责管理自己的状态。Android框架提供的视图都实现了onSaveInstanceState()onRestoreInstanceState(),因此不必在fragment中管理视图状态(自定义视图也应该实现这两个方法)。

在布局中有id的视图才能保留其状态,没有id的视图无法保留状态。

3.2 局部变量

Fragment负责管理局部变量中的状态数据。可以使用onSaveInstanceState(Bundle)保存数据。当fragment被重新创建时,可以在onCreate()onCreateView()onViewCreated()方法中访问bundle中的数据。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RandomGoodDeedFragment extends Fragment {
    private boolean isEditing;
    private String randomGoodDeed;
    private RandomGoodDeedViewModel viewModel;
    ...

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(IS_EDITING_KEY, isEditing);
        outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState != null) {
            isEditing = savedInstanceState.getBoolean(IS_EDITING_KEY, false);
            randomGoodDeed = savedInstanceState.getString(RANDOM_GOOD_DEED_KEY);
        } else {
            randomGoodDeed = viewModel.generateRandomGoodDeed();
        }
    }
}

注:也可以使用ViewModel来保存UI状态,这样就无需手动保存和恢复,详见ViewModel概览

4.与fragment通信

为了正确响应用户事件和共享状态信息,通常需要在activity和fragment之间或者两个fragment之间进行通信。

Fragment库提供了两种通信方式:共享ViewModel和Fragment Result API。如果需要与自定义API共享持久性数据,则使用ViewModel。对于数据可放在Bundle中的一次性结果,使用Fragment Result API。

4.1 使用ViewModel共享数据

ViewModel对象用于存储和管理UI数据,详细信息参见ViewModel概览

4.1.1 与宿主activity共享数据

在某些情况下,可能需要在fragment与其宿主activity之间共享数据。

例如,考虑下面的ItemViewModel

1
2
3
4
5
6
7
8
9
10
11
public class ItemViewModel extends ViewModel {
    private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>();

    public void selectItem(Item item) {
        selectedItem.setValue(item);
    }

    public LiveData<Item> getSelectedItem() {
        return selectedItem;
    }
}

LiveData是可感知生命周期的、可观测的数据容器类,详细信息参见LiveData概览

Fragment及其宿主activity都可以通过将activity传入ViewModelProvider构造器来获取ViewModel的共享实例。二者都可以观察和修改其中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
    private ItemViewModel viewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
        viewModel.getSelectedItem().observe(this, item -> {
            // Perform an action with the latest item data.
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ListFragment extends Fragment {
    private ItemViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class);
        ...
        items.setOnClickListener(item -> {
            // Set a new item.
            viewModel.selectItem(item);
        });
    }
}

注:如果ListFragment以自身为作用域(即new ViewModelProvider(this)),将会得到与MainActivity不同的ViewModel实例。

4.1.2 在fragment之间共享数据

同一activity中的两个或多个fragment通常需要相互通信。

例如,假设一个fragment显示一个列表,另一个fragment允许用户对该列表应用各种过滤器。这两个fragment可以使用其activity为作用域共享一个ViewModel来处理这种通信。这样fragment不需要相互了解,activity也不需要做任何事来辅助通信。

下面的示例展示了两个fragment如何使用共享的ViewModel进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ListViewModel extends ViewModel {
    private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>();

    private final LiveData<List<Item>> originalList = ...;
    private final LiveData<List<Item>> filteredList = ...;

    public LiveData<List<Item>> getFilteredList() {
        return filteredList;
    }

    public LiveData<Set<Filter>> getFilters() {
        return filters;
    }

    public void addFilter(Filter filter) { ... }

    public void removeFilter(Filter filter) { ... }
}
1
2
3
4
5
6
7
8
9
10
11
12
public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FilterFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilters().observe(getViewLifecycleOwner(), set -> {
            // Update the selected filters UI.
        });
    }

    public void onFilterSelected(Filter filter) {
        viewModel.addFilter(filter);
    }

    public void onFilterDeselected(Filter filter) {
        viewModel.removeFilter(filter);
    }
}

4.1.3 在父fragment与子fragment之间共享数据

为了在父fragment与子fragment之间共享数据,使用父fragment作为ViewModel作用域,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(this).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        }
    }
}
1
2
3
4
5
6
7
8
public class ChildFragment extends Fragment {
    private ListViewModel viewModel;
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class);
        ...
    }
}

4.2 使用Fragment Result API获取结果

在某些情况下,可能需要在两个fragment之间或fragment与其宿主activity之间传递一次性的值。例如,一个fragment扫描二维码,并将数据传回之前的fragment。

在Fragment库1.3.0及更高版本中,FragmentManager类实现了FragmentResultOwner接口,这意味着FragmentManager可以充当fragment结果的集中存储。这使得fragment可以通过设置和监听结果进行通信,而无需相互引用。

4.2.1 在fragment之间传递结果

为了将数据从fragment B传回fragment A,首先在fragment A(即接收结果的fragment)中设置结果监听器。对fragment A的FragmentManager调用setFragmentResultListener(),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
            // We use a String here, but any type that can be put in a Bundle is supported.
            String result = bundle.getString("bundleKey");
            // Do something with the result.
        }
    });
}

在fragment B(即生成结果的fragment)中,通过在同一个FragmentManager上调用setFragmentResult()使用相同的requestKey设置结果:

1
2
3
4
5
6
7
8
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

然后,一旦fragment A处于STARTED状态,它就会收到结果并执行监听器回调。

fragment之间传递结果

对于给定的键只能有一个监听器和结果。如果对同一个键多次调用setFragmentResult(),并且监听fragment未处于STARTED状态,则新结果会替换旧结果。

4.2.2 在父fragment与子fragment之间传递结果

为了将结果从子fragment传递给父fragment,在父fragment中调用setFragmentResultListener()时使用getChildFragmentManager()而不是getParentFragmentManager()

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Set the listener on the child fragmentManager.
    getChildFragmentManager()
        .setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                String result = bundle.getString("bundleKey");
                // Do something with the result.
            }
        });
}

子fragment使用getParentFragmentManager()设置结果:

1
2
3
4
5
6
7
8
9
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        // The child fragment needs to still set the result on its parent fragment manager.
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

父fragment与子fragment之间传递结果

4.2.3 在宿主activity中接收结果

为了在宿主activity中接收fragment结果,使用getSupportFragmentManager()设置结果监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                // We use a String here, but any type that can be put in a Bundle is supported.
                String result = bundle.getString("bundleKey");
                // Do something with the result.
            }
        });
    }
}

5.测试fragment

https://developer.android.google.cn/guide/fragments/test

This post is licensed under CC BY 4.0 by the author.