Android google samples nowinandroid app模块流程分析

googe samples之 nowinandroid项目地址:https://github.com/android/nowinandroid

1.app模块build.gradle文件配置

plugins {
    id("nowinandroid.android.application")
    id("nowinandroid.android.application.compose")
    id("nowinandroid.android.application.jacoco")
    id("nowinandroid.android.hilt")
    id("jacoco")
    id("nowinandroid.firebase-perf")
}

引入了application插件,compose插件,jacoco插件,hilt插件,firebase插件

依赖项:

    implementation(project(":feature:interests"))
    implementation(project(":feature:foryou"))
    implementation(project(":feature:bookmarks"))
    implementation(project(":feature:topic"))
    implementation(project(":feature:settings"))

    implementation(project(":core:common"))
    implementation(project(":core:ui"))
    implementation(project(":core:designsystem"))
    implementation(project(":core:data"))
    implementation(project(":core:model"))

    implementation(project(":sync:work"))

feature文件夹下是app模块的界面上拆分组件。
如feature_interests 就是兴趣tab
feature_foryou 就是第一个tab
feature_bookmarks 是书签模块
等等

下面的core就是核心模块了,有通用组件,ui组件,设计系统,数据区

然后sync_work是一些工具module了。

2.清单文件

    <application
        android:name=".NiaApplication"
        android:allowBackup="true"
        android:enableOnBackInvokedCallback="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Nia.Splash">
<!--        Profileable 是 Android 10 中引入的清单配置,可用于 CPU 和内存分析任务。使用 profileable 标志而不是 debuggable 标志具有降低性能开销的关键优势;-->
        <profileable android:shell="true" tools:targetApi="q" />

        <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>
    </application>

这里很简单,就一个Application自定义的,一个首页,说明这个App就一个Activity。
这里引入了一个profileable标签,是Android10后引入的,可以用于CPU和内存分析任务。

3.自定义Application

这里App开始启动了。
首先是用了一个HiltAndroidApp的注解。
这个是类似Dragger的依赖注解框架,这里要求在Application用这个注解。
关于Hilt用法可参考:https://developer.android.com/training/dependency-injection/hilt-android?hl=zh-cn

这里在Application里面初始化了一个东西:
Sync.initialize(context = this)
作用就是初始化Sync三方库。

里面是这样走的:

  AppInitializer.getInstance(context)
            .initializeComponent(SyncInitializer::class.java)

AppInitializer是google的Jetpack下的startup中的类成员,关于这个用法可以参考下郭霖的博客:https://blog.csdn.net/guolin_blog/article/details/108026357

为什么要引入这个同步的库呢?
/**

  • Registers work to sync the data layer periodically on app startup.
  • 初始化三方库 这里是WorkManager,主要用于
  • Android出于设备电量的考虑,为开发者提供了WorkManager,旨在将一些不需要及时完成的任务交给它来完成。
  • 虽然WorkManager宣称,能够保证任务得到执行,但我在真实设备中,发现应用程序彻底退出与重启设备,任务都没有再次执行。
  • 查阅了相关资料,发现这应该与系统有关系。我们前面也提到了,WorkManager会根据系统的版本,
  • 决定采用JobScheduler或是AlarmManager+Broadcast Receivers来完成任务。
  • 但是这些API很可能会受到OEM系统的影响。比如,假设某个系统不允许AlarmManager自动唤起,那么WorkManager很可能就无法正常使用。
    */

4.MainActivity 参数定义

Application走完,就到了Activity了。
首先是两个注解:

  1. @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    Opt-in 是 Kotlin 标准库中的一个方法,用于声明使用某些 API 需要明确的同意。
    该功能可以让开发者告知 API 使用者使用某些 API 需要一些特定的条件,如果使用者已经知晓则需要明确声明依旧使用(Opt-in)才能继续使用该 API。
    例如,某些 API 尚处于测试阶段,未来可能会发生变化;亦或是我前言中提到的场景,都非常适合使用该方法。
    如果我们声明了某个方法(functiuon)或类(class)需要 Opt-in ,则IDE或编译器会发出警告,要求使用者明确标注需要使用(Opt-in)。
    参考文档:https://juejin.cn/post/7157950373250990093

  2. @AndroidEntryPoint
    在要使用依赖注入的类上方添加@AndroidEntryPoint注解
    参考文档:https://www.jianshu.com/p/22a36660a656

然后声明了两个注入的变量:

/**
     * Lazily inject [JankStats], which is used to track jank throughout the app.
     * JankStats 是首个专为在用户设备上检测及报告应用的性能问题而构建的 AndroidX 库。
     * JankStats 是占用空间相对较小的 API,主要有三大目标: 捕获每帧的性能信息、在用户设备 (不仅是开发设备) 上运行、以及在应用出现性能问题时启用检测,并报告所发生的情况
     *
     * 在Dagger中Lazy可以将要注入的依赖项转变为懒加载模式,这样注入的依赖项,只有在需要使用时,
     * 才会调用对应的依赖生成方法。懒加载的实现很简单,只需要在注入位置将依赖项的类型设置为Lazy接口的类型参数即可。
     *
     * 构造函数在di 文件夹下的 JankStatsModule 下
     */
    @Inject
    lateinit var lazyStats: dagger.Lazy<JankStats>

    /**
     * 自定义网络监视器 这个是一个接口 应该也是测试用的
     */
    @Inject
    lateinit var networkMonitor: NetworkMonitor

关于lazyStats怎么初始化的?在项目di文件夹下有个JankStatsModule

/**
 * 这里的目标是生产一个JankStats对象
 *
 * 怎么生产呢,需要2个构造参数
 * 1.JankStats.OnFrameListener
 * 2.Window
 *
 * window需要依赖activity,这里就需要 ActivityComponent了
 * 这个JankStats对象的生命周期设定为Activity级别
 */
@Module
@InstallIn(ActivityComponent::class)
object JankStatsModule {
    @Provides
    fun providesOnFrameListener(): JankStats.OnFrameListener {
        return JankStats.OnFrameListener { frameData ->
            // Make sure to only log janky frames.
            if (frameData.isJank) {
                // We're currently logging this but would better report it to a backend.
                Log.v("NiA Jank", frameData.toString())
            }
        }
    }

    @Provides
    fun providesWindow(activity: Activity): Window {
        return activity.window
    }

    @Provides
    fun providesJankStats(
        window: Window,
        frameListener: JankStats.OnFrameListener
    ): JankStats {
        return JankStats.createAndTrack(window, frameListener)
    }
}

这里生产了 一个JankStats对象。

关于网络监视器如何初始化的呢?
这个网络监视器是一个接口来的:

/**
 * Utility for reporting app connectivity status
 */
interface NetworkMonitor {
    val isOnline: Flow<Boolean>
}

一直没找到如何实例化的地方。
不过再其它模块,core下的data.di文件夹下发现了另外一个DataModule下有如下代码:


    @Binds
    fun bindsNetworkMonitor(
        networkMonitor: ConnectivityManagerNetworkMonitor
    ): NetworkMonitor

这里貌似可以生产一个NetworkMonitor对象。
但怎么关联起来的呢?

这个我确实不清楚hilt的用法,而且这个DataModule理应需要在Application配置,但也没找到相关依赖,可能hilt对加上这种注解能全局自动生成依赖代码也说不定。反正这里的网络监视器应该就是这个ConnectivityManagerNetworkMonitor 主要逻辑也在这里。

第三个变量是viewModel
val viewModel: MainActivityViewModel by viewModels()
这个用法类似:
ViewModelProvider(this).get(MainActivityViewModel::class.java)

5.首页生命周期-onCreate

继续走,第一行是 适配闪屏页 installSplashScreen()
关于闪屏页可参考文档:https://blog.csdn.net/guolin_blog/article/details/120275319(郭霖)

然后这里监听了一个密封接口中的 Loading 状态,
密封接口定义如下:

/**
 * 首页ui 状态的密封类,有加载中和成功的状态
 * 密封接口 类似枚举类型,说明有Loading状态和成功状态
 * 表示受限制的类层次结构
 */
sealed interface MainActivityUiState {
    object Loading : MainActivityUiState
    data class Success(val userData: UserData) : MainActivityUiState
}

很明显这里Loading实现了这个接口,Success也实现了这个接口。

在onCreate监听了这个Loading类:


        /**
         * mutableStateOf 表明某个变量是有状态的,对变量进行监听,当状态改变时,触发重绘。
         * remember --- 记录变量的值,使得下次使用改变量时不进行初始化。
         * 使用 remember 存储对象的可组合项会创建内部状态,使该可组合项有状态。
         * remember 会为函数提供存储空间,将 remember 计算的值储存,当 remember 的键改变的时候会进行重新计算值并储存。
         * rememberSaveable --- 在这里顺带提一下,rememberSaveable 可以在重组后保持状态,也可以在重新创建 activity 和进程后保持状态。
         */
        var uiState: MainActivityUiState by mutableStateOf(Loading)

然后开启一个携程,拿用户数据:

 // Activity生命周期内使用携程
        lifecycleScope.launch {
            // Lifecycle.State.STARTED: 表示只有在Activity处于Started状态的情况下,协程中的代码才会执行。
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState
                    .onEach {
                        uiState = it
                    }
                    .collect() // 走collect方法才代表执行携程
            }
        }

拿到数据后,才回去更新uiState对象。

就是说uiState会有上面密封接口定义的两种类型。

当监听到uiState变化后会走:

 /**
         * 如果网络请求正在请求loading状态,就还是显示闪屏,如果拿到数据了就不需要闪屏了
         */
        splashScreen.setKeepOnScreenCondition {
            when (uiState) {
                Loading -> true
                is Success -> false
            }
        }

闪屏会去除掉。

下面开启沉浸式状态栏:

  /**
         * 第二参数decorFitsSystemWindows表示是否沉浸,false 表示沉浸,true表示不沉浸
         */
        WindowCompat.setDecorFitsSystemWindows(window, false)

然后就是内容区,compose设置内容区了
一个setContent的函数,太长了,就不全部贴图,挨个分析。

1.首先获取一个systemUIController

 /**
             * 开发者若想在 Compose 布局中控制 System UI,就必须获取 SystemUiController 对象。
             * 通过该库提供 rememberSystemUiController 函数,开发者可以获取当前操作系统(目前仅支持 Android 系统)的  SystemUiController 对象。
             */
            val systemUiController = rememberSystemUiController()

2.是否使用深色主题,用户可以自己选

 /**
             * 是否使用深色主题,入参当前ui状态
             */
            val darkTheme = shouldUseDarkTheme(uiState)
            

内部实现:

/**
 * Returns `true` if dark theme should be used, as a function of the [uiState] and the
 * current system context.
 * 调用@Composable 函数的函数必须用@Composable 注解标记
 */
@Composable
private fun shouldUseDarkTheme(
    uiState: MainActivityUiState,
): Boolean = when (uiState) {
    /**
     * isSystemInDarkTheme 根据系统来,加载中状态
     */
    Loading -> isSystemInDarkTheme()
    is Success -> when (uiState.userData.darkThemeConfig) {
        /**
         * 成功状态下,根据用户配置的数据来展示,如果跟随系统那就跟随,如果用户主动设置了某一种,那就成功时选择
         */
        DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
        DarkThemeConfig.LIGHT -> false
        DarkThemeConfig.DARK -> true
    }
}

然后开启一个DisposableEffect来关联上面这两个对象:

  /**
             * disposable,顾名思义,这就是一个自带清理作用的Effect
             */
            DisposableEffect(systemUiController, darkTheme) {
                // 否与导航栏图标+内容是否“黑暗"
                systemUiController.systemBarsDarkContentEnabled = !darkTheme
                onDispose {}
            }

最后,开始设置App主题了:

  /**
             * 主题配置,是否暗色,判断用户是否有自己选择
             * 是否用Android主题
             * 下面这些代码才是内容区的布局,上面MainActivityUiState 这个只是拿用户的持久化数据,槽了
             */
            NiaTheme(
                darkTheme = darkTheme,
                androidTheme = shouldUseAndroidTheme(uiState)
            ) {
                // NiaApp是一个闭包,作为第三个参数
                NiaApp(
                    /**
                     * 窗口大小类,获取一个WindowSizeClass 这个实例
                     */
                    windowSizeClass = calculateWindowSizeClass(this),
                    /**
                     * 网络监视器,前面注入的
                     */
                    networkMonitor = networkMonitor,
                )
            }

里面的闭包 NiaApp也就是开启了我们的App。

6.首页其它生命周期

  override fun onResume() {
        super.onResume()
        /**
         * 跟踪性能
         */
        lazyStats.get().isTrackingEnabled = true
    }

    override fun onPause() {
        super.onPause()
        /**
         * 去除跟踪
         */
        lazyStats.get().isTrackingEnabled = false
    }

MainActivity内容就这么多了,非常指简洁。
布局交给NiaApp类处理了。

其它对象初始化交给hilt处理了。

主要就只开了个协程获取用户数据,而且优化闪屏页和loading的显示。

7.NiaTheme主题设置

先是主题颜色:

 // 主题颜色
    val colorScheme = when {
        // 判断当前 android主题是暗色还是浅色
        androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
        // 没有禁用动态主题并且支持动态主题
        !disableDynamicTheming && supportsDynamicTheming() -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        // 其它情况
        else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
    }

渐变色:

    // Gradient colors 渐变色
    // 空渐变色
    val emptyGradientColors = GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
    // 默认渐变色
    val defaultGradientColors = GradientColors(
        top = colorScheme.inverseOnSurface,
        bottom = colorScheme.primaryContainer,
        container = colorScheme.surface
    )
    // 渐变色
    val gradientColors = when {
        androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors
        !disableDynamicTheming && supportsDynamicTheming() -> emptyGradientColors
        else -> defaultGradientColors
    }

最终设置到MaterialTheme里面:

   // Composition locals
    CompositionLocalProvider(
        LocalGradientColors provides gradientColors,
        LocalBackgroundTheme provides backgroundTheme
    ) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = NiaTypography,
            content = content
        )
    }

8.NiaApp状态定义

首先创建一个NiaApp实例。
入参:窗口大小类,网络监视器

 NiaApp(
                    /**
                     * 窗口大小类,获取一个WindowSizeClass 这个实例
                     */
                    windowSizeClass = calculateWindowSizeClass(this),
                    /**
                     * 网络监视器,前面注入的
                     */
                    networkMonitor = networkMonitor,
                )

这里是App首页ui样式。

这里用到的状态作为第三个参数,作为app状态记录。

函数定义如下:

@OptIn(
    ExperimentalMaterial3Api::class,
    ExperimentalLayoutApi::class,
    ExperimentalComposeUiApi::class,
    ExperimentalLifecycleComposeApi::class
)
@Composable
fun NiaApp(
    windowSizeClass: WindowSizeClass,
    networkMonitor: NetworkMonitor,
    /**
     * app状态记录
     */
    appState: NiaAppState = rememberNiaAppState(
        networkMonitor = networkMonitor,
        windowSizeClass = windowSizeClass
    ),
) 

这里第三个参数已经默认实现了。

就是用于app状态记录,调用了一个自定义的rememberNiaAppState方法。

@Composable
fun rememberNiaAppState(
    windowSizeClass: WindowSizeClass,
    networkMonitor: NetworkMonitor,
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    /**
     * 导航控制器
     */
    navController: NavHostController = rememberNavController()
): NiaAppState {
    NavigationTrackingSideEffect(navController)
    /**
     * remember 里面参数可以任意,闭包里面的东西,就是remember返回的东西
     */
    return remember(navController, coroutineScope, windowSizeClass, networkMonitor) {
        NiaAppState(navController, coroutineScope, windowSizeClass, networkMonitor)
    }
}

这里默认实现了协程作用域,导航控制器,需要返回一个NiaAppState变量。

这里通过remember闭包来返回这个变量。

这个NiaAppState定义如下:

/**
 * 主要用于类似LiveData保存的数据
 * App状态记录,需要传入导航控制器,携程作用范围,window窗口大小
 */
@Stable
class NiaAppState(
    val navController: NavHostController,
    val coroutineScope: CoroutineScope,
    val windowSizeClass: WindowSizeClass,
    networkMonitor: NetworkMonitor,
) {

类似LiveData,保存了首页必要的一些数据。

 /**
     * 当前在那个tab
     */
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination
 /**
     * 通过在哪个tab,决定用哪个枚举类, TopLevelDestination是自己定义的枚举类
     */
    val currentTopLevelDestination: TopLevelDestination?
        @Composable get() = when (currentDestination?.route) {
            forYouNavigationRoute -> FOR_YOU
            bookmarksRoute -> BOOKMARKS
            interestsRoute -> INTERESTS
            else -> null
        }
 /**
     * 是否展示设置弹框
     */
    var shouldShowSettingsDialog by mutableStateOf(false)
        private set
 /**
     * 是否展示底部bar
     */
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
  /**
     * 是否展示水平左侧导航
     */
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar
 /**
     * 监控网络,是否断网
     */
    val isOffline = networkMonitor.isOnline
        .map(Boolean::not)
        .stateIn(
            scope = coroutineScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = false
        )
   /**
     * 这里是一个枚举类,表示底部tab集合
     * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
     * route.
     */
    val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList()
 /**
     * 点击tab事件传递
     *
     * @param topLevelDestination: The destination the app needs to navigate to.
     */
    fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
        trace("Navigation: ${topLevelDestination.name}") {
            val topLevelNavOptions = navOptions {
                // Pop up to the start destination of the graph to
                // avoid building up a large stack of destinations
                // on the back stack as users select items
                popUpTo(navController.graph.findStartDestination().id) {
                    saveState = true
                }
                // Avoid multiple copies of the same destination when
                // reselecting the same item
                launchSingleTop = true
                // Restore state when reselecting a previously selected item
                restoreState = true
            }

            when (topLevelDestination) {
                FOR_YOU -> navController.navigateToForYou(topLevelNavOptions)
                BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions)
                INTERESTS -> navController.navigateToInterestsGraph(topLevelNavOptions)
            }
        }
    }

然后定义了一些方法:

 /**
     * 返回点击
     */
    fun onBackClick() {
        navController.popBackStack()
    }

    /**
     * 改变设置弹框状态
     */
    fun setShowSettingsDialog(shouldShow: Boolean) {
        shouldShowSettingsDialog = shouldShow
    }

9.NiaApp内容区

App背景色

@Composable
fun NiaBackground(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val color = LocalBackgroundTheme.current.color
    val tonalElevation = LocalBackgroundTheme.current.tonalElevation
    Surface(
        color = if (color == Color.Unspecified) Color.Transparent else color,
        tonalElevation = if (tonalElevation == Dp.Unspecified) 0.dp else tonalElevation,
        modifier = modifier.fillMaxSize(),
    ) {
        CompositionLocalProvider(LocalAbsoluteTonalElevation provides 0.dp) {
            content()
        }
    }
}

数据源使用 LocalBackgroundTheme.current.color

内容区继续渐变背景:

@Composable
fun NiaGradientBackground(
    modifier: Modifier = Modifier,
    gradientColors: GradientColors = LocalGradientColors.current,
    content: @Composable () -> Unit
) {
    val currentTopColor by rememberUpdatedState(gradientColors.top)
    val currentBottomColor by rememberUpdatedState(gradientColors.bottom)
    Surface(
        color = if (gradientColors.container == Color.Unspecified) {
            Color.Transparent
        } else {
            gradientColors.container
        },
        modifier = modifier.fillMaxSize()
    ) {
        Box(
            Modifier
                .fillMaxSize()
                .drawWithCache {
                    // Compute the start and end coordinates such that the gradients are angled 11.06
                    // degrees off the vertical axis
                    val offset = size.height * tan(
                        Math
                            .toRadians(11.06)
                            .toFloat()
                    )

                    val start = Offset(size.width / 2 + offset / 2, 0f)
                    val end = Offset(size.width / 2 - offset / 2, size.height)

                    // Create the top gradient that fades out after the halfway point vertically
                    val topGradient = Brush.linearGradient(
                        0f to if (currentTopColor == Color.Unspecified) {
                            Color.Transparent
                        } else {
                            currentTopColor
                        },
                        0.724f to Color.Transparent,
                        start = start,
                        end = end,
                    )
                    // Create the bottom gradient that fades in before the halfway point vertically
                    val bottomGradient = Brush.linearGradient(
                        0.2552f to Color.Transparent,
                        1f to if (currentBottomColor == Color.Unspecified) {
                            Color.Transparent
                        } else {
                            currentBottomColor
                        },
                        start = start,
                        end = end,
                    )

                    onDrawBehind {
                        // There is overlap here, so order is important
                        drawRect(topGradient)
                        drawRect(bottomGradient)
                    }
                }
        ) {
            content()
        }
    }
}

这里在上面0到0.724区间设置了topGradient
在0.2552f到1设置了 bottomGradient

10.业务内容区

   // 记录下snackbar状态,后续可能会刷新
            val snackbarHostState = remember { SnackbarHostState() }

这个用于网络监听,如果没有网络,底部会常驻一个snackbar

            // 是否离线
            val isOffline by appState.isOffline.collectAsStateWithLifecycle()

            // If user is not connected to the internet show a snack bar to inform them.
            // 如果没有网络,底部展示Snackbar
            val notConnectedMessage = stringResource(R.string.not_connected)
            // 这个是Compose里面的launch,compose开启携程的方法
            LaunchedEffect(isOffline) {
                if (isOffline) snackbarHostState.showSnackbar(
                    message = notConnectedMessage,
                    duration = Indefinite
                )
            }

Kotlin
// 是否应该展示设置弹框,当点击右上角设置后,会弹出设置弹框
if (appState.shouldShowSettingsDialog) {
SettingsDialog(
onDismiss = { appState.setShowSettingsDialog(false) }
)
}


然后就是脚手架了:
Scaffold,这里可以配置底部bar
```Kotlin
  Scaffold(
                modifier = Modifier.semantics {
                    testTagsAsResourceId = true
                },
                containerColor = Color.Transparent,
                contentColor = MaterialTheme.colorScheme.onBackground,
                contentWindowInsets = WindowInsets(0, 0, 0, 0),
                snackbarHost = { SnackbarHost(snackbarHostState) },
                // 底部bar 设置destinations,就是底部tab数据
                bottomBar = {
                    if (appState.shouldShowBottomBar) {
                        NiaBottomBar(
                            destinations = appState.topLevelDestinations,
                            onNavigateToDestination = appState::navigateToTopLevelDestination,
                            currentDestination = appState.currentDestination,
                            modifier = Modifier.testTag("NiaBottomBar")
                        )
                    }
                }
            )

这里snackbarHost也是脚手架里面的内容。

然后里面是一个Row,为什么是一个水平布局,因为考虑到兼容电视,平板,这里将导航页设置了一个水平的左侧导航。

  // 平板 水平布局 左侧导航
                    if (appState.shouldShowNavRail) {
                        NiaNavRail(
                            destinations = appState.topLevelDestinations,
                            onNavigateToDestination = appState::navigateToTopLevelDestination,
                            currentDestination = appState.currentDestination,
                            modifier = Modifier
                                .testTag("NiaNavRail")
                                .safeDrawingPadding()
                        )
                    }

然后就是实际内容区域了:
设置+主页构成。

// 垂直布局,状态栏+内容区
                    /**
                     * 以前,我们在布局中去设置一个控件的大小,间距,点击事件,宽高,背景等属性值。
                     * 而在Compose中我们是通过Modifie去设置,Modifie相当于一个控件的属性配置的工具类。
                     */
                    Column(Modifier.fillMaxSize()) {
                        // Show the top app bar on top level destinations.
                        val destination = appState.currentTopLevelDestination
                        if (destination != null) {
                            NiaTopAppBar(
                                titleRes = destination.titleTextId,
                                actionIcon = NiaIcons.Settings,
                                actionIconContentDescription = stringResource(
                                    id = settingsR.string.top_app_bar_action_icon_description
                                ),
                                colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
                                    containerColor = Color.Transparent
                                ),
                                /**
                                 * 设置点击事件触发appState中的 mutableStateOf变量发生更改
                                 */
                                onActionClick = { appState.setShowSettingsDialog(true) }
                            )
                        }

                        // 主页面
                        NiaNavHost(
                            navController = appState.navController,
                            onBackClick = appState::onBackClick
                        )
                    }

NiaTopAppBar是自定义顶部标题栏
NiaNavHost是主页面

主页面也是在App模块下的navigation文件夹中定义。

@Composable
fun NiaNavHost(
    /**
     * 导航控制器
     */
    navController: NavHostController,
    /**
     * 返回点击
     */
    onBackClick: () -> Unit,
    /**
     * 属性配置工具
     */
    modifier: Modifier = Modifier,
    /**
     * 开始路由设置
     */
    startDestination: String = forYouNavigationRoute
) {
    NavHost(
        navController = navController,
        startDestination = startDestination,
        modifier = modifier,
    ) {
        forYouScreen()
        bookmarksScreen()
        interestsGraph(
            navigateToTopic = { topicId ->
                navController.navigateToTopic(topicId)
            },
            nestedGraphs = {
                topicScreen(onBackClick)
            }
        )
    }
}

这里使用了一个NavHost 的compose官方组件。

tab实现为:
forYouScreen 你的模块
bookmarksScreen 书签模块
interestsGraph 兴趣模块

这几个tab都是依赖于其它module了。
app自身module大致内容就这么多了。


   转载规则


《Android google samples nowinandroid app模块流程分析》 Jason 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录