Fragment实例精讲——底部导航栏的实现(方法3)

感谢你继续深入探讨“Fragment实例精讲——底部导航栏的实现(方法3)”。结合你之前对Android四大组件(ActivityServiceBroadcastReceiverContentProvider)、Intent、复杂数据传递以及Fragment底部导航栏的实现(方法1:BottomNavigationView直接切换,方法2:ViewPager2+BottomNavigationView),我推测你希望学习另一种实现底部导航栏的方式,称为“方法3”。基于上下文和Android开发的常见实践,“方法3”很可能是指使用Jetpack Navigation组件(AndroidX Navigation)结合BottomNavigationViewFragment,这是现代Android开发中推荐的导航方案,提供声明式导航图、类型安全导航和与Fragment的深度集成。

以下我将以通俗易懂的中文,详细讲解如何使用Jetpack Navigation实现底部导航栏,包含完整代码、步骤解析和注意事项,延续方法1和方法2的场景(HomeFragmentTaskFragmentFileFragment),并整合IntentServiceBroadcastReceiverDocumentsProvider(参考“ContentProvider再探”)。如果你对“方法3”有其他定义(例如自定义View或其他方案),或需要特定功能(如复杂数据传递、动画效果、与ViewModel集成),请进一步说明,我可以调整方案。


一、方法3:Jetpack Navigation + BottomNavigationView + Fragment

1. 方法3概述

  • 核心组件
  • Jetpack Navigation:AndroidX提供的导航框架,通过导航图(Navigation Graph)声明Fragment之间的关系,支持类型安全导航和参数传递。
  • BottomNavigationView:提供底部导航栏,与Navigation组件联动,点击导航项切换Fragment。
  • NavController:管理导航操作,控制Fragment切换和导航栈。
  • 特点
  • 声明式导航:通过XML导航图定义导航关系,减少手动FragmentTransaction
  • 类型安全:支持Safe Args插件,确保参数传递安全。
  • 深层链接:支持外部链接直接导航到特定Fragment。
  • 与BottomNavigationView集成:官方提供便捷API,简化导航逻辑。
  • 与方法1和方法2的区别
  • 方法1(BottomNavigationView):手动通过FragmentManager替换Fragment,无滑动效果,代码较简单。
  • 方法2(ViewPager2):支持滑动切换,缓存Fragment,适合动态交互,但需要手动同步导航状态。
  • 方法3(Jetpack Navigation):声明式管理导航,代码更简洁,适合复杂导航场景,官方推荐。

2. 项目目标

  • 功能:实现一个底部导航栏,包含三个导航项:首页HomeFragment)、任务TaskFragment)、文件FileFragment),通过点击导航项切换Fragment。
  • HomeFragment:显示欢迎信息。
  • TaskFragment:输入任务,添加至列表,启动LogService记录,发送广播通知CustomReceiver
  • FileFragment:打开文件选择器(结合DocumentsProvider),显示选中的文件URI。
  • 组件交互
  • 使用Intent在Fragment间传递数据,启动Service或发送广播。
  • 集成DocumentsProvider(参考“ContentProvider再探”)选择文件。
  • 通过Safe Args传递任务数据(演示类型安全)。
  • 场景:用户通过点击底部导航栏切换页面,执行任务管理、文件选择等操作,导航逻辑由Navigation组件管理。

3. 实现步骤

步骤1:添加依赖

app/build.gradle中添加AndroidX、Navigation和Material Design依赖:

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.fragment:fragment:1.5.7'
    implementation 'androidx.navigation:navigation-fragment:2.5.3'
    implementation 'androidx.navigation:navigation-ui:2.5.3'
    // Safe Args插件(类型安全参数传递)
    apply plugin: 'androidx.navigation.safeargs'
}

在项目根目录的build.gradle中启用Safe Args插件:

buildscript {
    repositories {
        google()
    }
    dependencies {
        classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3'
    }
}
步骤2:创建导航图

res/navigation/nav_graph.xml中定义导航图,包含三个Fragment。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.HomeFragment"
        android:label="首页" />

    <fragment
        android:id="@+id/taskFragment"
        android:name="com.example.TaskFragment"
        android:label="任务">
        <argument
            android:name="taskName"
            app:argType="string"
            android:defaultValue="" />
    </fragment>

    <fragment
        android:id="@+id/fileFragment"
        android:name="com.example.FileFragment"
        android:label="文件" />
</navigation>

说明

  • 定义三个Fragment:homeFragmenttaskFragmentfileFragment
  • taskFragment添加参数taskName(演示Safe Args)。
  • 设置startDestinationhomeFragment
步骤3:复用Fragment

复用方法1和方法2的HomeFragmentTaskFragmentFileFragment,稍作调整以支持Safe Args和Navigation。

HomeFragment(欢迎页面):

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;

public class HomeFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_home, container, false);
        TextView welcomeText = view.findViewById(R.id.welcome_text);
        welcomeText.setText("欢迎使用应用!");
        return view;
    }
}

布局res/layout/fragment_home.xml):同方法1。

TaskFragment(任务管理,支持Safe Args):

import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;

public class TaskFragment extends Fragment {
    private TextView taskListText;
    private StringBuilder taskList = new StringBuilder();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_task, container, false);
        EditText taskInput = view.findViewById(R.id.task_input);
        Button addTaskBtn = view.findViewById(R.id.add_task_btn);
        taskListText = view.findViewById(R.id.task_list_text);

        // 获取Safe Args参数
        String initialTask = TaskFragmentArgs.fromBundle(getArguments()).getTaskName();
        if (!initialTask.isEmpty()) {
            taskList.append(initialTask).append("\n");
            taskListText.setText(taskList.toString());
        }

        addTaskBtn.setOnClickListener(v -> {
            String task = taskInput.getText().toString();
            if (!task.isEmpty()) {
                taskList.append(task).append("\n");
                taskListText.setText(taskList.toString());

                // 启动Service
                Intent serviceIntent = new Intent(getActivity(), LogService.class);
                serviceIntent.putExtra("log_message", "添加任务: " + task);
                getActivity().startService(serviceIntent);

                // 发送广播
                Intent broadcastIntent = new Intent("com.example.CUSTOM_ACTION");
                broadcastIntent.putExtra("task", task);
                getActivity().sendBroadcast(broadcastIntent);

                // 使用Safe Args导航回TaskFragment(演示参数传递)
                TaskFragmentDirections.ActionTaskFragmentSelf action =
                        TaskFragmentDirections.actionTaskFragmentSelf(task);
                Navigation.findNavController(v).navigate(action);

                taskInput.setText("");
            }
        });

        return view;
    }
}

布局res/layout/fragment_task.xml):同方法1。

说明

  • 使用TaskFragmentArgs(由Safe Args生成)获取taskName参数。
  • 添加任务后,通过Safe Args导航到自身,传递新任务名称。

FileFragment(文件选择):

import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.fragment.app.Fragment;

public class FileFragment extends Fragment {
    private static final int REQUEST_OPEN_DOCUMENT = 1;
    private TextView fileText;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_file, container, false);
        Button openFileBtn = view.findViewById(R.id.open_file_btn);
        fileText = view.findViewById(R.id.file_text);

        openFileBtn.setOnClickListener(v -> {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("text/plain");
            startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
        });

        return view;
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_OPEN_DOCUMENT && resultCode == RESULT_OK && data != null) {
            fileText.setText("选择的文件: " + data.getData());
        }
    }
}

布局res/layout/fragment_file.xml):同方法1。

步骤4:创建MainActivity

MainActivity配置NavControllerBottomNavigationView

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.bottomnavigation.BottomNavigationView;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 设置NavController
        NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
                .findFragmentById(R.id.nav_host_fragment);
        NavController navController = navHostFragment.getNavController();

        // 设置BottomNavigationView
        BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
        NavigationUI.setupWithNavController(bottomNav, navController);
    }
}

布局文件res/layout/activity_main.xml):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:navGraph="@navigation/nav_graph" />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

菜单文件res/menu/bottom_nav_menu.xml):

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/homeFragment"
        android:icon="@android:drawable/ic_menu_info_details"
        android:title="首页" />
    <item
        android:id="@+id/taskFragment"
        android:icon="@android:drawable/ic_menu_agenda"
        android:title="任务" />
    <item
        android:id="@+id/fileFragment"
        android:icon="@android:drawable/ic_menu_upload"
        android:title="文件" />
</menu>

说明

  • 使用FragmentContainerView作为NavHostFragment容器,加载导航图。
  • NavigationUI.setupWithNavController自动关联BottomNavigationViewNavController,简化导航逻辑。
  • 菜单项ID必须与导航图中的Fragment ID一致。
步骤5:复用Service和BroadcastReceiver

复用方法1和方法2的LogServiceCustomReceiver

// LogService.java
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class LogService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String message = intent.getStringExtra("log_message");
        if (message != null) {
            try {
                File file = new File(getFilesDir(), "app_log.txt");
                FileWriter writer = new FileWriter(file, true);
                writer.append(message).append("\n");
                writer.close();
                Log.d("LogService", "记录日志: " + message);
            } catch (IOException e) {
                Log.e("LogService", "日志记录失败", e);
            }
        }
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
// CustomReceiver.java
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class CustomReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String task = intent.getStringExtra("task");
        Log.d("CustomReceiver", "收到广播: 新任务 - " + task);
    }
}
步骤6:复用DocumentsProvider

复用之前的TextDocumentsProvider(参考“ContentProvider再探”),确保FileFragment能打开文件选择器。

步骤7:注册组件

AndroidManifest.xml中声明:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".LogService" />
        <receiver android:name=".CustomReceiver">
            <intent-filter>
                <action android:name="com.example.CUSTOM_ACTION" />
            </intent-filter>
        </receiver>
        <provider
            android:name=".TextDocumentsProvider"
            android:authorities="com.example.textdocuments"
            android:exported="true"
            android:grantUriPermissions="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>
</manifest>
步骤8:运行与测试
  1. 部署应用,启动MainActivity,验证底部导航栏是否显示“首页”“任务”“文件”。
  2. 点击导航项,检查是否切换到HomeFragmentTaskFragmentFileFragment
  3. TaskFragment输入任务,验证:
  • 任务是否添加到列表。
  • LogService是否记录日志(检查app_log.txt)。
  • CustomReceiver是否在Logcat打印广播。
  • Safe Args是否正确传递任务名称(导航后显示新任务)。
  1. FileFragment打开文件选择器,验证是否显示TextDocumentsProvider的文件并返回URI.
  2. 测试屏幕旋转,确保导航状态自动恢复(由Navigation组件管理)。

三、方法3实现的关键点

1. Jetpack Navigation配置

  • 导航图:在res/navigation/nav_graph.xml定义Fragment和导航关系。
  • NavController:通过NavHostFragment获取,控制导航:
  NavController navController = NavHostFragment.findNavController(fragment);
  • BottomNavigationView集成
  NavigationUI.setupWithNavController(bottomNav, navController);
  • Safe Args
  • 自动生成TaskFragmentArgsTaskFragmentDirections类。
  • 传递参数:
    java TaskFragmentDirections.ActionTaskFragmentSelf action = TaskFragmentDirections.actionTaskFragmentSelf(task); navController.navigate(action);

2. Fragment管理

  • 自动管理:Navigation组件自动处理FragmentTransaction,无需手动replace
  • 导航栈:支持后退栈(Back Stack),自动处理返回键。
  • 状态保存:Navigation组件自动保存和恢复导航状态,无需手动处理onSaveInstanceState

3. 与Intent和组件结合

  • IntentTaskFragment通过Intent启动Service和广播,FileFragment启动文件选择器。
  • Service:异步记录任务日志。
  • BroadcastReceiver:接收任务更新通知。
  • DocumentsProvider:提供文件访问,结合SAF。

4. 性能与优化

  • 性能
  • Navigation组件优化Fragment切换,减少手动管理开销。
  • 使用NavHostFragment确保单一容器,降低内存占用。
  • 用户体验
  • 添加导航动画(在nav_graph.xml中):
    xml <action android:id="@+id/action_taskFragment_self" app:destination="@id/taskFragment" app:enterAnim="@anim/nav_enter" app:exitAnim="@anim/nav_exit" />
  • 定义动画(res/anim/nav_enter.xml):
    xml <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromXDelta="100%" android:toXDelta="0%" android:duration="300" /> </set>
  • 深层链接:支持外部链接导航到特定Fragment:
  <deepLink
      android:id="@+id/deep_link_task"
      app:uri="example://task/{taskName}" />

5. 与方法1和方法2的对比

  • 方法1(BottomNavigationView):手动管理Fragment切换,简单但代码冗余,适合小型项目。
  • 方法2(ViewPager2):支持滑动切换,缓存Fragment,适合需要滑动体验的场景,但需手动同步导航。
  • 方法3(Jetpack Navigation):声明式导航,代码简洁,类型安全,官方推荐,适合复杂导航。
  • 选择建议
  • 小型项目:方法1,简单快速。
  • 需要滑动:方法2,动态流畅。
  • 复杂导航或现代化开发:方法3,规范高效。

6. 常见问题

  • 导航图ID不匹配:确保bottom_nav_menu.xml的ID与nav_graph.xml一致。
  • Safe Args错误:检查build.gradle是否正确配置Safe Args插件。
  • Fragment重复创建:Navigation组件自动管理,避免手动new Fragment。
  • 文件选择器无反应:确保DocumentsProviderauthorities正确。

四、复杂数据传递(补充)

结合你之前的“Intent之复杂数据传递”,可以在TaskFragment中使用Safe Args传递复杂数据(如Parcelable对象)。

修改导航图

<fragment
    android:id="@+id/taskFragment"
    android:name="com.example.TaskFragment"
    android:label="任务">
    <argument
        android:name="taskObj"
        app:argType="com.example.Task"
        app:nullable="true" />
</fragment>

Task类(实现Parcelable):

import android.os.Parcel;
import android.os.Parcelable;

public class Task implements Parcelable {
    private String title;
    private String status;

    public Task(String title, String status) {
        this.title = title;
        this.status = status;
    }

    protected Task(Parcel in) {
        title = in.readString();
        status = in.readString();
    }

    public static final Creator<Task> CREATOR = new Creator<Task>() {
        @Override
        public Task createFromParcel(Parcel in) {
            return new Task(in);
        }

        @Override
        public Task[] newArray(int size) {
            return new Task[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(title);
        dest.writeString(status);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public String getTitle() {
        return title;
    }

    public String getStatus() {
        return status;
    }
}

在TaskFragment中传递

Task task = new Task(taskInput.getText().toString(), "进行中");
TaskFragmentDirections.ActionTaskFragmentSelf action =
        TaskFragmentDirections.actionTaskFragmentSelf(null).setTaskObj(task);
Navigation.findNavController(v).navigate(action);

接收

Task taskObj = TaskFragmentArgs.fromBundle(getArguments()).getTaskObj();
if (taskObj != null) {
    taskList.append(taskObj.getTitle()).append(": ").append(taskObj.getStatus()).append("\n");
    taskListText.setText(taskList.toString());
}

五、学习与实践资源(中文)

  • 官方文档
  • Android开发者 – Navigation
  • Android开发者 – BottomNavigationView
  • 书籍:《Android编程权威指南》(中文版,Big Nerd Ranch出品)
  • 课程:B站或慕课网的Android开发课程,搜索“Android Jetpack Navigation”。
  • 实践
  • 扩展示例,添加导航动画或深层链接。
  • 结合ViewModel管理TaskFragment的任务列表。

六、下一步

为了更精准地讲解Fragment与底部导航栏(方法3),请告诉我:

  1. 你对“方法3”的具体定义(是否特指Jetpack Navigation,或其他方案如自定义View)?
  2. 你希望聚焦的功能(Safe Args、动画、深层链接、ViewModel)?
  3. 当前技术水平(熟悉Fragment、了解Navigation组件)?
  4. 是否需要更详细的代码(如复杂数据传递、Navigation与Room集成)?
  5. 是否探索其他方法(例如方法4:自定义View)?

你可以基于上述示例扩展功能(如添加ViewModel或深层链接),或提出具体问题,我会提供更深入的代码或分析!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注