Skip to content

Commit

Permalink
ASAA-159 - Move NavWrapper from projects to Launchpad Compose (#4)
Browse files Browse the repository at this point in the history
* ASAA-159 - Move NavWrapper from projects to Launchpad Compose

* ASAA-159 - Run KtLint

* ASAA-159 - Fixed KtLint to not remove trailing commas
  • Loading branch information
br-Emery authored Jan 16, 2024
1 parent d04ff1c commit 7ff2c82
Show file tree
Hide file tree
Showing 22 changed files with 843 additions and 5 deletions.
14 changes: 10 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
[versions]
agp = "8.2.0"
agp = "8.2.1"
androidx-window = "1.2.0"
compose-plugin = "1.5.11"
kotlin = "1.9.21"
kt-lint-gradle = "11.6.1"
compose-plugin = "1.5.11"
navigation-compose = "2.7.5"
navigation-compose = "2.7.6"
precompose = "1.5.10"


compile-sdk = "34"
min-sdk = "24"
launchpad-compose = "0.0.1"
launchpad-compose = "0.0.2"

[libraries]
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "androidx-window" }
compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
precompose = { group = "moe.tlaster", name = "precompose", version.ref = "precompose" }

[plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" }
Expand Down
4 changes: 4 additions & 0 deletions kmp-launchpad-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ kotlin {
androidMain.dependencies {
implementation(compose.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.window)
implementation(libs.compose.material3.window.size)
}
commonMain.dependencies {
api(libs.precompose)
implementation(compose.animation)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.runtime)
implementation(compose.ui)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.bottlerocketstudios.launchpad.compose.example.navigation

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.collectAsState
import com.bottlerocketstudios.launchpad.compose.navigation.NavigationWrapper
import com.bottlerocketstudios.launchpad.compose.navigation.util.createDevicePostureFlow
import com.bottlerocketstudios.launchpad.compose.navigation.util.getWindowWidthSize
import moe.tlaster.precompose.PreComposeApp

class MainActivityExample : ComponentActivity() {

// Create a flow that emits the current device posture.
private val devicePostureFlow = createDevicePostureFlow()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Collects the device posture flow and stores it in a state variable.
val devicePosture = devicePostureFlow.collectAsState()

PreComposeApp {
// In this example, the NavWrapper for the Precompose Navigation library is being used.
// The app param will extend the wrappers navigation component and bottom bar allowing both to be used in App Composable.
NavigationWrapper(
widthSize = getWindowWidthSize(this),
devicePosture = devicePosture.value,
navigationItems = exampleNavigationItems
) { navigator, bottomBar ->
ExampleApp(
widthSize = getWindowWidthSize(this),
navigator = navigator,
bottomBar = bottomBar
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[*.{kt,kts}]
# More info about .editorconfig at https://github.com/pinterest/ktlint#custom-editorconfig-properties
# possible values: number (e.g. 2) OR unset (no quotes - makes ktlint ignore indentation completely)
indent_size=4
# true (recommended) / false
insert_final_newline=true
# possible values: number (e.g. 120) (package name, imports & comments are ignored) OR off (no quotes)
# it's automatically set to 100 on `ktlint --android ...` (per Android Kotlin Style Guide)
# that is just not long enough IMO - using 200 for now
max_line_length=200
# Makes git commit history look good and appending items to lists easier
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.bottlerocketstudios.launchpad.compose.navigation

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.PermanentDrawerSheet
import androidx.compose.material3.PermanentNavigationDrawer
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.bottlerocketstudios.launchpad.compose.navigation.components.LaunchpadBottomAppBar
import com.bottlerocketstudios.launchpad.compose.navigation.components.LaunchpadDrawerContent
import com.bottlerocketstudios.launchpad.compose.navigation.components.LaunchpadNavigationRail
import com.bottlerocketstudios.launchpad.compose.navigation.utils.DevicePosture
import com.bottlerocketstudios.launchpad.compose.navigation.utils.NavigationItem
import com.bottlerocketstudios.launchpad.compose.navigation.utils.NavigationType
import com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass
import com.bottlerocketstudios.launchpad.compose.navigation.utils.generateNavItems
import com.bottlerocketstudios.launchpad.compose.navigation.utils.toNavigationType

/**
* A composable function that wraps the Androidx Compose Navigation component.
* *
* @param widthSize The current window width size class.
* @param devicePosture The current device posture.
* @param navigationItems The list of navigation items, these will be the items that show up on the navigation rail or bottom navigation bar.
* For an example of this please see [exampleNavigationItems] and the [NavigationItem] class.
* @param app The function that renders the app content.
*/
@Composable
fun AndroidNavigationWrapper(
widthSize: WindowWidthSizeClass,
devicePosture: DevicePosture,
navigationItems: List<NavigationItem>,
app: @Composable (navHostController: NavHostController?, bottomBar: (@Composable () -> Unit)) -> Unit,
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val navController = rememberNavController()
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.destination?.route
val navItems = remember {
generateNavItems(navigationItems) {
navController.navigate(it.route)
}
}
val navigationType = remember(widthSize, devicePosture) {
derivedStateOf {
widthSize.toNavigationType(devicePosture)
}
}

when (navigationType.value) {
NavigationType.PERMANENT_NAVIGATION_DRAWER -> PermanentNavigationDrawer(
drawerContent = { PermanentDrawerSheet { LaunchpadDrawerContent(navItems) { it == currentRoute } } },
) { app(navController) { } }

NavigationType.MODAL_NAVIGATION -> ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = { ModalDrawerSheet { LaunchpadDrawerContent(navItems) { it == currentRoute } } },
) { app(navController) { } }

else -> Row {
AnimatedVisibility(
visible = navigationType.value == NavigationType.NAVIGATION_RAIL,
enter = slideInVertically(animationSpec = spring(stiffness = Spring.StiffnessHigh)),
exit = slideOutHorizontally(animationSpec = spring(stiffness = Spring.StiffnessHigh)),
) {
LaunchpadNavigationRail(navItems) { it == currentRoute }
}

Column(
modifier = Modifier
.fillMaxSize(),
) {
app(navController) {
AnimatedVisibility(
visible = navigationType.value == NavigationType.BOTTOM_NAVIGATION,
enter = slideInVertically(animationSpec = spring(stiffness = Spring.StiffnessHigh)),
exit = slideOutHorizontally(animationSpec = spring(stiffness = Spring.StiffnessHigh)),
) {
LaunchpadBottomAppBar(navItems) { it == currentRoute }
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bottlerocketstudios.launchpad.compose.navigation.util

import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import com.bottlerocketstudios.launchpad.compose.resources.Dimensions
import com.bottlerocketstudios.launchpad.compose.resources.SMALL_SCREEN_WIDTH_DP
import com.bottlerocketstudios.launchpad.compose.resources.smallDimensions
import com.bottlerocketstudios.launchpad.compose.resources.sw360Dimensions

/**
* Returns the dimensions of the screen.
*
* If the screen width is less than or equal to 360dp, the small dimensions are returned.
* Otherwise, the sw360 dimensions are returned.
*/
@Composable
fun getDimensions(): Dimensions = if (LocalConfiguration.current.screenWidthDp <= SMALL_SCREEN_WIDTH_DP) smallDimensions else sw360Dimensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bottlerocketstudios.launchpad.compose.navigation.util

import android.app.Activity
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass

/**
* Gets the window width size class of the given activity.
*
* @param activity The activity to get the window width size class for.
* @return The window width size class of the activity.
*/
@Composable
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
fun getWindowWidthSize(activity: Activity): WindowWidthSizeClass = calculateWindowSizeClass(activity).widthSizeClass.toLocalModel()
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.bottlerocketstudios.launchpad.compose.navigation.util

import androidx.activity.ComponentActivity
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.window.layout.WindowInfoTracker
import com.bottlerocketstudios.launchpad.compose.navigation.utils.DevicePosture
import com.bottlerocketstudios.launchpad.compose.navigation.utils.FoldingFeature
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/**
* Creates a flow that emits the current device posture.
*
* The flow emits the following values:
*
* * [DevicePosture.NormalPosture] when the device is not in a book or separating posture.
* * [DevicePosture.BookPosture] when the device is in a book posture.
* * [DevicePosture.Separating] when the device is in a separating posture.
*
* The flow is lifecycle-aware and will stop emitting values when the activity is paused.
*
* @return The flow that emits the current device posture.
*/
fun ComponentActivity.createDevicePostureFlow() = WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
.flowWithLifecycle(this.lifecycle)
.map { layoutInfo ->
val foldingFeature =
layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
when {
isBookPosture(foldingFeature) ->
DevicePosture.BookPosture(foldingFeature.bounds)

isSeparating(foldingFeature) ->
DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)

else -> DevicePosture.NormalPosture
}
}
.stateIn(
scope = lifecycleScope,
started = SharingStarted.Eagerly,
initialValue = DevicePosture.NormalPosture
)

/**
* Converts an Android [WindowWidthSizeClass] to a Launchpad [WindowWidthSizeClass].
*
* @return The converted [WindowWidthSizeClass].
*/
internal fun WindowWidthSizeClass.toLocalModel(): com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass = when (this) {
WindowWidthSizeClass.Compact -> com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass.Compact
WindowWidthSizeClass.Medium -> com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass.Medium
WindowWidthSizeClass.Expanded -> com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass.Expanded
else -> com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass.Compact
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bottlerocketstudios.launchpad.compose.navigation.util

import com.bottlerocketstudios.launchpad.compose.navigation.utils.FoldingFeature
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

@OptIn(ExperimentalContracts::class)
internal fun isBookPosture(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

@OptIn(ExperimentalContracts::class)
internal fun isSeparating(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.bottlerocketstudios.launchpadcompose.widget.listdetail
package com.bottlerocketstudios.launchpad.compose.widget.listdetail

import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.bottlerocketstudios.launchpad.compose.example.navigation

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bottlerocketstudios.launchpad.compose.navigation.utils.WindowWidthSizeClass
import moe.tlaster.precompose.navigation.NavHost
import moe.tlaster.precompose.navigation.Navigator
import moe.tlaster.precompose.navigation.transition.NavTransition

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExampleApp(
widthSize: WindowWidthSizeClass,
navigator: Navigator?,
bottomBar: @Composable () -> Unit
) {
Scaffold(
topBar = { TopAppBar(title = { Text("Example App") }) },
bottomBar = bottomBar
) {
navigator?.let { navController ->
NavHost(
// Assign the navigator to the NavHost
navigator = navController,
// Navigation transition for the scenes in this NavHost, this is optional
navTransition = NavTransition(),
// The start destination
initialRoute = "/home",
modifier = Modifier.padding(it)
) {
// Navgraph goes here
}
}
}
}
Loading

0 comments on commit 7ff2c82

Please sign in to comment.