Impl base structure of bottom-navigation

This commit is contained in:
Shcherbatykh Oleg
2026-02-24 19:00:27 +03:00
parent 4794b0e185
commit 37c926ac77
19 changed files with 345 additions and 47 deletions

View File

@@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint
import ru.fincode.tsudesk.ui.TSUDeskApp
import ru.fincode.tsudesk.presentation.TSUDeskApp
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

View File

@@ -0,0 +1,14 @@
package ru.fincode.tsudesk
import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
import ru.fincode.tsudesk.presentation.navigation.AppNavHost
@Composable
fun TSUDeskRoot() {
val navController = rememberNavController()
AppNavHost(
navController = navController
)
}

View File

@@ -1,11 +1,11 @@
package ru.fincode.tsudesk.ui
package ru.fincode.tsudesk.presentation
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import ru.fincode.tsudesk.ui.navigation.AppNavHost
import ru.fincode.tsudesk.presentation.navigation.AppNavHost
@Composable
fun TSUDeskApp() {
@@ -16,9 +16,7 @@ fun TSUDeskApp() {
modifier = Modifier,
color = MaterialTheme.colorScheme.background
) {
AppNavHost(
navController = navController
)
AppNavHost(navController = navController)
}
}
}

View File

@@ -0,0 +1,37 @@
package ru.fincode.tsudesk.app.presentation.main
import androidx.navigation.NavBackStackEntry
import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.core.navigation.TopLevelDestination
import ru.fincode.tsudesk.core.navigation.route
fun selectedTopLevel(entry: NavBackStackEntry?): TopLevelDestination {
if (entry == null) return TopLevelDestination.SCHEDULE
return runCatching {
when (entry.route<AppRoute>()) {
AppRoute.Schedule,
is AppRoute.ScheduleDetails -> TopLevelDestination.SCHEDULE
AppRoute.News,
is AppRoute.NewsDetails -> TopLevelDestination.NEWS
AppRoute.Progress -> TopLevelDestination.PROGRESS
AppRoute.Settings -> TopLevelDestination.SETTINGS
else -> TopLevelDestination.SCHEDULE
}
}.getOrDefault(TopLevelDestination.SCHEDULE)
}
fun shouldShowBottomBar(entry: NavBackStackEntry?): Boolean {
if (entry == null) return false
return runCatching {
when (entry.route<AppRoute>()) {
AppRoute.Splash -> false
else -> true
}
}.getOrDefault(true)
}

View File

@@ -0,0 +1,82 @@
package ru.fincode.tsudesk.presentation.main
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import ru.fincode.core.ui.components.BottomBarItem
import ru.fincode.core.ui.components.TsudeskBottomBar
import ru.fincode.tsudesk.R
import ru.fincode.tsudesk.app.presentation.main.selectedTopLevel
import ru.fincode.tsudesk.app.presentation.main.shouldShowBottomBar
import ru.fincode.tsudesk.core.navigation.TopLevelDestination
import ru.fincode.tsudesk.core.navigation.navigateToTopLevel
@Composable
fun MainScaffold(
navController: NavHostController,
content: @Composable (Modifier) -> Unit,
) {
val scheduleIcon = painterResource(R.drawable.ic_progress)
val newsIcon = painterResource(R.drawable.ic_news)
val progressIcon = painterResource(R.drawable.ic_progress)
val settingsIcon = painterResource(R.drawable.ic_progress)
val items = listOf(
TopLevelItem(
destination = TopLevelDestination.SCHEDULE,
labelRes = R.string.tab_schedule,
icon = scheduleIcon
),
TopLevelItem(
destination = TopLevelDestination.NEWS,
labelRes = R.string.tab_news,
icon = newsIcon
),
TopLevelItem(
destination = TopLevelDestination.PROGRESS,
labelRes = R.string.tab_progress,
icon = progressIcon
),
TopLevelItem(
destination = TopLevelDestination.SETTINGS,
labelRes = R.string.tab_settings,
icon = settingsIcon
),
)
val backStackEntry by navController.currentBackStackEntryAsState()
val selected = selectedTopLevel(backStackEntry)
Scaffold(
bottomBar = {
if (shouldShowBottomBar(backStackEntry)) {
val uiItems = items.map { item ->
BottomBarItem(
key = item.destination.name,
label = stringResource(item.labelRes),
icon = item.icon
)
}
TsudeskBottomBar(
items = uiItems,
selectedKey = selected.name,
onItemClick = { clicked ->
if (clicked.key == selected.name) return@TsudeskBottomBar
navController.navigateToTopLevel(
TopLevelDestination.valueOf(clicked.key)
)
}
)
}
}
) { innerPadding ->
content(Modifier.padding(innerPadding))
}
}

View File

@@ -0,0 +1,12 @@
package ru.fincode.tsudesk.presentation.main
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import ru.fincode.tsudesk.core.navigation.TopLevelDestination
data class TopLevelItem(
val destination: TopLevelDestination,
@StringRes val labelRes: Int,
val icon: Painter,
)

View File

@@ -0,0 +1,83 @@
package ru.fincode.tsudesk.presentation.navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import ru.fincode.tsudesk.presentation.main.MainScaffold
import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.core.navigation.navigateRoute
import ru.fincode.tsudesk.feature.schedule.presentation.screen.ScheduleRoute
import ru.fincode.tsudesk.feature.splash.presentation.screen.SplashRoute
@Composable
fun AppNavHost(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = AppRoute.Splash
) {
composable<AppRoute.Splash> {
SplashRoute(
onFinish = {
navController.navigateRoute(AppRoute.Main) {
popUpTo(AppRoute.Splash) { inclusive = true }
launchSingleTop = true
}
}
)
}
navigation<AppRoute.Main>(startDestination = AppRoute.Schedule) {
composable<AppRoute.Schedule> {
MainScaffold(navController) { innerModifier ->
ScheduleRoute(
modifier = innerModifier,
// onOpenDetails = { lessonId ->
// navController.navigateRoute(AppRoute.ScheduleDetails(lessonId))
// }
)
}
}
composable<AppRoute.News> {
MainScaffold(navController) { innerModifier: Modifier ->
// NewsScreen(
// modifier = innerModifier,
// onOpenDetails = { id ->
// navController.navigateRoute(AppRoute.NewsDetails(id))
// }
// )
}
}
composable<AppRoute.Progress> {
MainScaffold(navController) { innerModifier: Modifier ->
// ProgressScreen(modifier = innerModifier)
}
}
composable<AppRoute.Settings> {
MainScaffold(navController) { innerModifier ->
// SettingsRoute(modifier = innerModifier)
}
}
//
// composable<AppRoute.ScheduleDetails> { entry ->
// val args = entry.route<AppRoute.ScheduleDetails>()
// ScheduleDetailsScreen(lessonId = args.lessonId)
// }
//
// composable<AppRoute.NewsDetails> { entry ->
// val args = entry.route<AppRoute.NewsDetails>()
// NewsDetailsScreen(id = args.id)
// }
}
}
}

View File

@@ -1,29 +0,0 @@
package ru.fincode.tsudesk.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.navigation
import ru.fincode.tsudesk.core.navigation.AppRoute
import ru.fincode.tsudesk.feature.schedule.presentation.navigation.scheduleGraph
import ru.fincode.tsudesk.feature.splash.presentation.navigation.splashGraph
@Composable
fun AppNavHost(
navController: NavHostController
) {
NavHost(
navController = navController,
startDestination = AppRoute.Splash
) {
splashGraph(navController)
navigation<AppRoute.Main>(
startDestination = AppRoute.Schedule
) {
scheduleGraph()
// newsGraph(navController)
// progressGraph(navController)
}
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,5h14v14z"
android:fillColor="?attr/colorControlNormal"/>
<path
android:pathData="M7,7h10v2H7zM7,11h10v2H7zM7,15h7v2H7z"
android:fillColor="?attr/colorControlNormal"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,3L1,9l11,6 9,-4.91V17h2V9L12,3z"
android:fillColor="?attr/colorControlNormal"/>
<path
android:pathData="M5,12.18V17c0,2.21 3.58,4 7,4s7,-1.79 7,-4v-4.82l-7,3.82 -7,-3.82z"
android:fillColor="?attr/colorControlNormal"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,4h-1L18,2h-2v2L8,4L8,2L6,2v2L5,4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM19,20L5,20L5,10h14v10zM19,8L5,8L5,6h14v2z"
android:fillColor="?attr/colorControlNormal"/>
</vector>

View File

@@ -1,3 +1,7 @@
<resources>
<string name="app_name">TSUDesk</string>
<string name="tab_schedule">Расписание</string>
<string name="tab_news">Новости</string>
<string name="tab_progress">Успеваемость</string>
<string name="tab_settings">Настройки</string>
</resources>

View File

@@ -39,6 +39,7 @@ dependencies {
// Compose
implementation(platform(libs.compose.bom))
implementation(libs.compose.runtime)
implementation("androidx.compose.material:material-icons-extended")
// Navigation Compose
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.navigation.common)

View File

@@ -25,6 +25,9 @@ sealed interface AppRoute {
@Serializable
data object Progress : AppRoute
@kotlinx.serialization.Serializable
data object Settings : AppRoute
@Serializable
data class ScheduleDetails(
val lessonId: Long

View File

@@ -4,19 +4,19 @@ import kotlinx.serialization.Serializable
@Serializable
enum class TopLevelDestination {
SCHEDULE,
NEWS,
PROGRESS
SCHEDULE, NEWS, PROGRESS, SETTINGS
}
fun TopLevelDestination.toRoute(): AppRoute = when (this) {
TopLevelDestination.SCHEDULE -> AppRoute.Schedule
TopLevelDestination.NEWS -> AppRoute.News
TopLevelDestination.PROGRESS -> AppRoute.Progress
TopLevelDestination.SETTINGS -> AppRoute.Settings
}
val TOP_LEVEL_DESTINATIONS: List<TopLevelDestination> = listOf(
TopLevelDestination.SCHEDULE,
TopLevelDestination.NEWS,
TopLevelDestination.PROGRESS
TopLevelDestination.PROGRESS,
TopLevelDestination.SETTINGS
)

View File

@@ -27,14 +27,27 @@ android {
sourceCompatibility = jvm
targetCompatibility = jvm
}
kotlinOptions {
jvmTarget = jvm.toString()
}
kotlinOptions { jvmTarget = jvm.toString() }
buildFeatures {
buildConfig = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtension.get()
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
// Compose UI
implementation(platform(libs.compose.bom))
implementation(libs.compose.runtime)
implementation(libs.compose.ui)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
debugImplementation(libs.compose.ui.tooling)
}

View File

@@ -0,0 +1,40 @@
package ru.fincode.core.ui.components
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
data class BottomBarItem(
val key: String,
val label: String,
val icon: Painter,
)
@Composable
fun TsudeskBottomBar(
items: List<BottomBarItem>,
selectedKey: String,
onItemClick: (BottomBarItem) -> Unit,
) {
NavigationBar {
items.forEach { item ->
NavigationBarItem(
selected = item.key == selectedKey,
onClick = { onItemClick(item) },
icon = {
Icon(
painter = item.icon,
contentDescription = item.label
)
},
label = {
Text(text = item.label)
},
alwaysShowLabel = true
)
}
}
}

View File

@@ -1,17 +1,22 @@
package ru.fincode.tsudesk.feature.schedule.presentation.screen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun ScheduleRoute(
viewModel: ScheduleViewModel = hiltViewModel()
modifier: Modifier = Modifier,
// onOpenDetails: (lessonId: Long) -> Unit,
viewModel: ScheduleViewModel = hiltViewModel(),
) {
val state = viewModel.state.collectAsStateWithLifecycle().value
val state by viewModel.state.collectAsStateWithLifecycle()
ScheduleScreen(
state = state,
onIntent = viewModel::onIntent
onIntent = viewModel::onIntent,
modifier = modifier
)
}

View File

@@ -12,12 +12,14 @@ fun SplashRoute(
viewModel: SplashViewModel = hiltViewModel()
) {
val state = viewModel.state.collectAsStateWithLifecycle().value
LaunchedEffect(Unit) {
LaunchedEffect(viewModel) {
viewModel.effects.collect { effect ->
when (effect) {
SplashEffect.OpenMain -> onFinish()
}
}
}
SplashScreen(state = state)
}