课程 4: 使用 CursorLoader 加载数据

2,250 阅读15分钟

这节课是 Android 开发(入门)课程 的第四部分《数据与数据库》的最后一节课,导师依然是 Jessica Lin 和 Katherine Kuan。这节课主要内容有两个部分:

  1. 利用上节课完成的 Content Provider 实现 CursorLoader,以在后台线程加载数据,使 UI 中的数据列表保持活跃状态。
  2. 对 Pets App 进行逻辑完善和细节优化。

关键词:CursorAdapter、CursorLoader、Notification URI、UP & BACK Button、AlertDialog、invalidateOptionsMenu & onPrepareOptionsMenu

CursorAdapter

在 Pets App 中,CatalogActivity 显示的列表使用 ListView 实现,自然需要应用适配器模式。之前在《课程 2: 数据,列表,循环和自定义类》中提到,适配器模式能够提供视图回收的好处,这是 Android 的一个重要内存策略。同时,适配器也决定了列表子项的布局,由于这里的数据来源是 Cursor,所以需要使用 Android 提供的 CursorAdapter 作为 ListView 的适配器。

CursorAdapter 是一个抽象类,需要 override 的方法有两个:

  • View newView(Context context, Cursor cursor, ViewGroup parent)
  • void bindView(View view, Context context, Cursor cursor)

两个方法配合工作,实现 ListView 列表各个的子项显示:

  1. 首先通过 newView 创建新的子项视图,此时视图不包含数据。
  2. 随后通过 bindView 将数据填充到视图中,其中输入参数
    (1)View view: 即 newView 已创建的视图。
    (2)Cursor cursor: 即要填充的 Cursor 数据,此时移动 Cursor 位置(行)的操作已自动完成。

每当显示新的 ListView 列表子项时,CursorAdapter 都要先通过 newView 创建新的子项视图,随后通过 bindView 将数据填充到视图中;而当视图回收时,CursorAdapter 就可以直接通过 bindView 将数据填充到回收的视图中,无需再通过 newView 创建新的子项视图,这也是 CursorAdapter 将 ListView 列表的子项显示分为 newViewbindView 两个方法实现的原因。

当然,这些过程都是自动完成的,开发者只需要 override 上述两个方法即可。例如在 Pets App 中,PetCursorAdapter 作为 CatalogActivity 的 ListView 的适配器,实现其列表显示。ListView 与 CursorAdapter 的更多应用信息可以参考这个 CodePath 教程

In PetCursorAdapter.java

public class PetCursorAdapter extends CursorAdapter {

    public PetCursorAdapter(Context context, Cursor c) {
        super(context, c, 0 /* flags */);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        TextView nameTextView = (TextView) view.findViewById(R.id.name);
        TextView summaryTextView = (TextView) view.findViewById(R.id.summary);

        int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);
        int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);

        String petName = cursor.getString(nameColumnIndex);
        String petBreed = cursor.getString(breedColumnIndex);

        if (TextUtils.isEmpty(petBreed)) {
            petBreed = context.getString(R.string.unknown_breed);
        }

        nameTextView.setText(petName);
        summaryTextView.setText(petBreed);
    }
}
  1. 在 PetCursorAdapter 构造函数内调用超级类的构造函数进行初始化,以继承 CursorAdapter 的特性。
  2. Override newView 方法直接返回根据 list_item 布局构造的 View 对象。
  3. Override bindView 方法将传入的 Cursor 数据填充至传入的 View 中,具体的做法是:
    (1)通过 view.findViewById 方法找到需要填充数据的视图,这里是两个 TextView。
    (2)通过 cursor.getColumnIndex 方法并根据 Contract 中定义的数据库列名找到所需的 Cursor 键名,随后通过 cursor.getString 方法获取对应键的值,这里是字符串。
    (3)通过 setText 方法将数据填充进视图中。
  4. 利用 TextUtils.isEmpty 方法可以判断字符串是否为空。

Note:
与 ArrayAdapter 通过 getView 方法来实现 ListView 列表的子项显示不同,CursorAdapter 需要使用 newViewbindView 两个方法。不过事实上,CursorAdapter 中也存在 getView 方法,从 源码 可以看出,getView 会根据当前情况下是否存在可回收的视图,来决定是否调用 newView 创建新的子项视图。
因此,CursorAdapter 在实现 ListView 列表的子项显示时,与 ArrayAdapter 一样调用 getView 方法,而 newViewbindView 两个方法更像是辅助方法的概念;只不过对于开发者而言,只需要 override newViewbindView 两个方法,无需关心其中的逻辑。

CursorLoader

设置好显示 Cursor 数据的列表后,接下来将利用上节课完成的 Content Provider 实现 CursorLoader,以在后台线程加载数据,使 UI 中的数据列表保持活跃状态,包括添加或删除数据时列表自动增加或减少一行数据。

CursorLoader 是 AsyncTaskLoader 的子类,它会通过 URI 查询 ContentResolver (不是 Content Provider)以获取 Cursor 对象。与《课程 3: 线程与并行》中提到的概念一样,CursorLoader 作为一种 Loader,它也会在后台线程中进行耗时较长的数据库查询任务,不会阻塞 UI 线程而导致 ANR;同时,CursorLoader 能够在数据变化时,使用相同的 URI 重新查询数据,这保证了 UI 中的数据列表始终处于最新状态。

因此,在 Pets App 中引入 CursorLoader 在后台线程加载数据,步骤与 AsyncTaskLoader 的类似:

一、引入 CursorLoader

In CatalogActivity.java

public class CatalogActivity extends AppCompatActivity 
          implements LoaderManager.LoaderCallbacks<Cursor> {

    private static final int PET_LOADER = 0;

    PetCursorAdapter mCursorAdapter;

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

        ...

        ListView petListView = (ListView) findViewById(R.id.list);

        mCursorAdapter = new PetCursorAdapter(this, null);
        petListView.setAdapter(mCursorAdapter);

        getLoaderManager().initLoader(PET_LOADER, null, this);
    }

    ...

}
  1. 一个 Activity 或 Fragment 内只有一个 LoaderManager,它可以管理多个 Loader;而不同 Loader 之间的唯一标识是 ID,它可以是任意数字。因此在 CatalogActivity 中定义一个全局常量作为 CursorLoader 的 ID,并传入 initLoader 方法。
  2. initLoader 方法的第三个输入参数是 Loader 的回调对象,设置为 this 表示回调对象即 Activity 本身,回调函数放在 Activity 内,在 Activity 类名后面添加 implements 参数。
  3. 在这里,确保 CursorAdapter 定义为全局变量,并在 onCreate 方法中新建一个对象,暂时将输入参数 Cursor 设为 null,并设置为 ListView 的适配器。

二、实现 LoaderManager.LoaderCallbacks 的三个回调函数

In CatalogActivity.java

@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
    String[] projection = {
            PetEntry._ID,
            PetEntry.COLUMN_PET_NAME,
            PetEntry.COLUMN_PET_BREED };

    return new CursorLoader(this,   // Parent activity context
            PetEntry.CONTENT_URI,   // Provider content URI to query
            projection,             // Columns to include in the resulting Cursor
            null,                   // No selection clause
            null,                   // No selection arguments
            null);                  // Default sort order
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    mCursorAdapter.swapCursor(data);
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mCursorAdapter.swapCursor(null);
}
  1. Activity 或 Fragment 内的 LoaderManager 会自动管理 Loader 对象,所以开发者几乎不需要直接操作 Loader,往往是通过回调函数来处理数据加载的事件。
  2. onCreateLoader 方法中,创建并返回一个新的 CursorLoader 对象。在此之后,CursorLoader 就会在后台线程开始查询数据,在查询结束后调用 onLoadFinished 方法,其输入参数包含获取的 Cursor 数据。
  3. 与在 Quake Report App 中使用 AsyncTaskLoader 的自定义类不同,CursorLoader 是一个具象类,所以它可以直接调用,关键输入参数为 Content URI 和想要读取的列数据(字符串数组,注意一定要有 "_id" 列)。
  4. onLoadFinished 方法中,调用 CursorAdapter 的 swapCursor 方法,将获取的 Cursor 数据传入 CursorAdapter 用于列表显示。在这里,开发者无需调用 cursor.close() 来释放资源,因为 CursorLoader 会自动处理无用的旧数据。
  5. onLoaderReset 方法中,同样调用 CursorAdapter 的 swapCursor 方法,但传入 null,表示清除 CursorAdapter 的数据,使列表清空。这种情况会在当前 Loader 被销毁,或者最新的 Cursor 数据无效时发生,同时这也会防止内存泄漏。

尽管 CursorLoader 设置完成了,但目前还没有建立一个数据变化与数据更新之间的沟通机制。CursorLoader 需要仅在数据变化时重新获取数据,在类似设备屏幕旋转、重启应用等情况时数据保持不变,避免不必要的数据重新加载。

已知应用在 UI 端与数据库端之间的数据传递主要依靠 URI 和 CRUD 对应的方法,所以最好的做法是在 Content Provider 的四个 CRUD 方法中建立一个依靠 URI 传递数据的沟通机制。也就是说:

  1. 在 Content Provider 的 query 方法中设置一个 Notification URI,表示 CursorLoader 需要观察数据变化的内容。
    (1)如果 Notification URI 为整个表格,那么表格中的任何数据变化都会触发 CursorLoader 重新加载数据。
    (2)如果 Notification URI 为整个表格的其中一行,如第三行,那么 CursorLoader 仅在该行发生变化时起作用,如新增一个第七行时不会触发 CursorLoader 动作。
  2. 分别在 Content Provider 的 insertupdatedelete 方法中通过 URI 告知 CursorLoader 数据发生了变化,使 CursorLoader 重新加载数据;

Query

因此,在 Pets App 中,首先在 Content Provider 的 query 方法内通过 setNotificationUri 方法设置 Notification URI,其中输入参数包含应用环境的 Content Resolver。

In PetProvider.java

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                    String sortOrder) {
    Cursor cursor;

    ...

    cursor.setNotificationUri(getContext().getContentResolver(), uri);

    return cursor;
}

Insert

在 Pets App 中,由于 Content Provider 的 insert 方法调用了 insertPet 辅助方法,所以在该方法内调用 ContentResolver 的 notifyChange 方法告知 CursorLoader 重新加载数据。其中第一个参数为 URI,第二个参数为可选的 ContentObserver 参数,传入 null 表示默认 CursorAdapter 将作为收到通知的对象,自动触发 CursorLoader 重新加载数据的动作。

In PetProvider.java

private Uri insertPet(Uri uri, ContentValues values) {
    ...

    getContext().getContentResolver().notifyChange(uri, null);

    ...
}

Update

类似地,对于 Content Provider 的 update 方法,在 updatePet 辅助方法内调用 ContentResolver 的 notifyChange 方法告知 CursorLoader 重新加载数据。不过在这里需要先检查是否真正发生了数据更新,若是才执行指令,避免不必要的重新加载。

In PetProvider.java

private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

    ...

    int rowsUpdated = database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs);

    if (rowsUpdated != 0) {
        getContext().getContentResolver().notifyChange(uri, null);
    }

    return rowsUpdated;
}

Delete

类似地,对于 Content Provider 的 delete 方法,在检查到真正发生了数据删除后,调用 ContentResolver 的 notifyChange 方法告知 CursorLoader 重新加载数据。

In PetProvider.java

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {

    ...

    if (rowsDeleted != 0) {
        getContext().getContentResolver().notifyChange(uri, null);
    }

    return rowsDeleted;
}

至此,CursorLoader 可以通过 Content Provider 在后台线程加载数据,并在数据变化时保持 UI 中的数据列表始终处于最新状态。而事实上,CursorLoader 的应用非常广泛,属于 Android 中最常用的一种 Loader。例如在 Pets App 的 EditorActivity 中,利用 CursorLoader 在后台线程获取由 Intent 传递过来的 URI 指向的 Cursor 数据,并把它们设置到相应的视图中。

In CatalogActivity.java

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

    ...

    petListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
            Intent intent = new Intent(CatalogActivity.this, EditorActivity.class);

            Uri currentPetUri = ContentUris.withAppendedId(PetEntry.CONTENT_URI, id);

            intent.setData(currentPetUri);

            startActivity(intent);
        }
    });
}
  1. 在 CatalogActivity 中,设置 ListView 的 OnItemClickListener 为 Intent 到 EditorActivity,并且通过 setData 方法带上被点击项目的 Content URI。
  2. 通过 ContentUris 的 withAppendedId 构造一个带被点击项目 ID 的 Content URI。

In EditorActivity.java

private static final int EXISTING_PET_LOADER = 0;

private Uri mCurrentPetUri;

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

    Intent intent = getIntent();
    mCurrentPetUri = intent.getData();

    if (mCurrentPetUri == null) {
        setTitle(getString(R.string.editor_activity_title_new_pet));
    } else {
        setTitle(getString(R.string.editor_activity_title_edit_pet));

        getLoaderManager().initLoader(EXISTING_PET_LOADER, null, this);
    }

    ...
}
  1. 在 EditorActivity 中,通过 getIntent().getData() 获取由 Intent 传递过来的 URI 数据。

  1. 由于 EditorActivity 存在“新建”和“编辑”两种模式,而两者可以根据是否有 Intent 传递的 Data 区分,所以在这里通过 if/else 语句判断两种模式,进行代码分流。其中,通过 setTitle 方法分别设置两种模式下 Activity 应用栏的标题,此时可以删去 AndroidManifest 中 EditorActivity 的 android:label 属性。
  2. 当 EditorActivity 处于“编辑”模式时,引入 CursorLoader,在后台线程获取由 Intent 传递过来的 URI 指向的 Cursor 数据,并把它们设置到相应的视图中。

In EditorActivity.java

public class EditorActivity extends AppCompatActivity implements
        LoaderManager.LoaderCallbacks<Cursor> {

    ...

    @Override
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
        String[] projection = {
                PetEntry._ID,
                PetEntry.COLUMN_PET_NAME,
                PetEntry.COLUMN_PET_BREED,
                PetEntry.COLUMN_PET_GENDER,
                PetEntry.COLUMN_PET_WEIGHT };

        return new CursorLoader(this,   // Parent activity context
                mCurrentPetUri,         // Query the content URI for the current pet
                projection,             // Columns to include in the resulting Cursor
                null,                   // No selection clause
                null,                   // No selection arguments
                null);                  // Default sort order
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        if (cursor == null || cursor.getCount() < 1) {
            return;
        }

        if (cursor.moveToFirst()) {
            int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);
            int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);
            int genderColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_GENDER);
            int weightColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_WEIGHT);

            String name = cursor.getString(nameColumnIndex);
            String breed = cursor.getString(breedColumnIndex);
            int gender = cursor.getInt(genderColumnIndex);
            int weight = cursor.getInt(weightColumnIndex);

            mNameEditText.setText(name);
            mBreedEditText.setText(breed);
            mWeightEditText.setText(Integer.toString(weight));

            switch (gender) {
                case PetEntry.GENDER_MALE:
                    mGenderSpinner.setSelection(1);
                    break;
                case PetEntry.GENDER_FEMALE:
                    mGenderSpinner.setSelection(2);
                    break;
                default:
                    mGenderSpinner.setSelection(0);
                    break;
            }
        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mNameEditText.setText("");
        mBreedEditText.setText("");
        mWeightEditText.setText("");
        mGenderSpinner.setSelection(0);
    }
}
  1. 与 CatalogActivity 中的 CursorLoader 类似,在 onCreateLoader 方法中创建并返回一个新的 CursorLoader 对象,其中 URI 输入参数就是由 Intent 传递过来的数据,在这里设置成了全局变量 mCurrentPetUri。在此之后,CursorLoader 就会在后台线程开始查询 URI 指定的 Cursor 数据,在查询结束后调用 onLoadFinished 方法,其输入参数包含获取的数据。
  2. onLoadFinished 中将传入的 Cursor 数据设置到相应的视图中,具体的做法与 CursorAdapter 的 bindView 方法类似。值得注意的有三点:
    (1)首先判断传入的 Cursor 数据是否为空,若是则提前返回,不执行任何其它操作。
    (2)由于正常情况下 Cursor 仅有一行数据,因此通过 if (cursor.moveToFirst() 判断语句确保仅在 Cursor 指向首行时执行任何其它操作。
    (3)通过 setSelection 设置 Spinner 默认选中的项目。
  3. 当 CursorLoader 被销毁时,在 onLoaderReset 中将相应的视图设为空或恢复为默认状态。

Override UP & BACK Button

在 Pets App 中,用户在 EditorActivity 点击向上 (UP) 或返回 (BACK) 按钮时,应用会直接退出 EditorActivity,而不会保存任何内容。因此,为了完善应用的业务逻辑,避免用户丢失工作,可以在这里弹出一个对话框,警告用户尚有未保存的更改。

一、监听是否进行了更改

In EditorActivity.java

private boolean mPetHasChanged = false;

private View.OnTouchListener mTouchListener = new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        mPetHasChanged = true;
        return false;
    }
};

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

    ...

    mNameEditText.setOnTouchListener(mTouchListener);
    mBreedEditText.setOnTouchListener(mTouchListener);
    mWeightEditText.setOnTouchListener(mTouchListener);
    mGenderSpinner.setOnTouchListener(mTouchListener);
}
  1. 定义一个全局变量 mPetHasChanged 作为编辑器是否被点击过的指示器,默认为 false,表示编辑器没有被点击;在 mTouchListener 监听器的 onTouch 方法中设为 true,表示有编辑器被点击。
  2. onCreate 方法中将四个编辑器的 OnTouchListener 设为 mTouchListener 监听器,这体现了监听器模式的优势,即一个监听器可以应用到多个视图中。

二、创建警告对话框的辅助方法

In EditorActivity.java

private void showUnsavedChangesDialog(DialogInterface.OnClickListener discardButtonClickListener) {
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage(R.string.unsaved_changes_dialog_msg);
    builder.setPositiveButton(R.string.discard, discardButtonClickListener);
    builder.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            if (dialog != null) {
                dialog.dismiss();
            }
        }
    });

    AlertDialog alertDialog = builder.create();
    alertDialog.show();
}

在这里用到了 AlertDialog,并把创建对话框的代码封装成一个方法,之前在《实战项目 9: 习惯记录应用》提到过;不同的是,这里要求传入一个 OnClickListener 作为 PositiveButton 的监听器,这种设计是因为向上 (UP) 与返回 (BACK) 按钮之间 PositiveButton 的操作不同。

三、Override BACK Button

In EditorActivity.java

@Override
public void onBackPressed() {
    if (!mPetHasChanged) {
        super.onBackPressed();
        return;
    }

    DialogInterface.OnClickListener discardButtonClickListener =
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    finish();
                }
            };
    showUnsavedChangesDialog(discardButtonClickListener);
}
  1. 自定义返回 (BACK) 按钮的逻辑需要 override onBackPressed 方法。
  2. onBackPressed 方法内,首先判断全局变量 mPetHasChanged 是否为真。若为假,说明没有编辑器被点击过,所以调用其超级类,使返回 (BACK) 按钮的逻辑保持默认;并提前返回结束方法,不再执行任何其它操作。
  3. 如果有任一编辑器被点击,首先定义 AlertDialog 的 PositiveButton 的 OnClickListener,在这里是直接调用 finish() 方法关闭 Activity;然后将适配器对象传入上述创建对话框的辅助方法,以弹出对话框供用户选择。

四、Override UP Button

In EditorActivity.java

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {

        ...

        case android.R.id.home:
            if (!mPetHasChanged) {
                NavUtils.navigateUpFromSameTask(EditorActivity.this);
                return true;
            }

            DialogInterface.OnClickListener discardButtonClickListener =
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                          NavUtils.navigateUpFromSameTask(EditorActivity.this);
                        }
                    };

            showUnsavedChangesDialog(discardButtonClickListener);
            return true;
    }
    return super.onOptionsItemSelected(item);
}
  1. 自定义向上 (UP) 按钮的逻辑需要在 onOptionsItemSelected 方法内添加一个 android.R.id.home case,注意不是 android.R.id.backandroid.R.id.up
  2. android.R.id.home case 下,首先判断全局变量 mPetHasChanged 是否为真。若为假,说明没有编辑器被点击过,所以调用 NavUtils 的 navigateUpFromSameTask 方法,关闭当前 Activity,将页面跳至其父级 Activity,即 CatalogActivity;并提前返回结束方法,不再执行任何其它操作。
  3. 如果有任一编辑器被点击,首先定义 AlertDialog 的 PositiveButton 的 OnClickListener,在这里是调用 NavUtils 的 navigateUpFromSameTask 方法,关闭当前 Activity,将页面跳至其父级 Activity,即 CatalogActivity;然后将适配器对象传入上述创建对话框的辅助方法,以弹出对话框供用户选择。

上述返回 (BACK) 和向上 (UP) 按钮的逻辑差别体现了两者导航模式的差别。正如《课程 5: Fragment》中提到的:

在导航的概念中,“向上”和“返回”按钮 (Up and Back buttons) 两者很容易混淆。

  • “向上”按钮通常位于屏幕的左上角。它返回的是本应用内层级结构中上一层的页面,直到本应用的主页,所以“向上”按钮不会跳出本应用。例如在邮件应用内,点击邮件详情页左上角的“向上”按钮会返回到邮件列表页,如果邮件列表页是应用的主页,那么这里通常没有“向上”按钮。
  • “返回”按钮显示在屏幕的底部,属于系统导航按钮 (Home、Menus、Back) 的其中一个,在一些 Android 设备上是实体按键。它返回的是按时间记录的上一个浏览页面,浏览页面不仅限于本应用,所以“返回”按钮有可能将用户导航到本应用外。例如当用户在观看视频时收到邮件提醒,如果用户点击提醒查看邮件详情,那么用户在邮件详情页点击“返回”按钮,就返回到先前的视频了,而不是返回到邮件列表页。 “返回”按钮还可用于关闭悬浮窗口,隐藏输入法,取消选中的高亮项目(如选中的文字以及弹出的“复制”操作栏)。

在运行时变更菜单选项

在 Pets App 中,EditorActivity 存在“新建”和“编辑”两种模式,其中“编辑”模式下有一个溢出菜单选项,用于删除当前数据,而“新建”模式应该隐藏这个选项。为了完善这一业务逻辑,应用需要在运行时变更菜单选项,这需要两个步骤。

首先,调用 invalidateOptionsMenu() 方法,告知 Android 当前菜单已发生变更,使应用执行 onPrepareOptionsMenu 方法,重新绘制菜单。关于 invalidateOptionsMenu() 方法作用的更多详解可以参考这个 stack overflow 帖子

最后,override onPrepareOptionsMenu 方法,通过全局变量 mCurrentPetUri 判断 EditorActivity 当前处于“新建”状态时,就通过 menu.findItem 方法找到需要变更的选项,随后利用选项的 setVisible 方法设置其可见性。

In EditorActivity.java

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    super.onPrepareOptionsMenu(menu);

    if (mCurrentPetUri == null) {
        MenuItem menuItem = menu.findItem(R.id.action_delete);
        menuItem.setVisible(false);
    }
    return true;
}