👉🏻 하단 탭 메뉴,드롭다운 메뉴에 대한 설명입니다.
Description of the bottom tab menu and drop-down menu.
👉🏻 스크린 파일은 다른 코틀린 파일로 분리되어 있습니다.
Screen file is separated into a different Kotlin file.
👉🏻 나머지 설명은 코드 주석을 참조 하세요
Please refer to the code comments for the rest of the explanation.
✔️ MainActivity.kt
package com.freelifemakers.bottomnavigationbar
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.freelifemakers.bottomnavigationbar.ui.theme.BottomNavigationBarTheme
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
// screen
import com.freelifemakers.bottomnavigationbar.ui.screens.HomeScreen
import com.freelifemakers.bottomnavigationbar.ui.screens.SearchScreen
import com.freelifemakers.bottomnavigationbar.ui.screens.SettingsScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BottomNavigationBarTheme {
// MainScreen 컴포저블을 호출하여 전체 화면 구조를 담당
// Call the MainScreen composable to take charge of the entire screen structure
MainScreen()
}
}
}
}
// 각 화면의 경로(Route)를 정의하는 sealed class
// sealed class : 봉인 클래스 같은 파일 내에서만 상속을 허용함.
// Sealed class that defines the route for each screen
// Sealed class: Inheritance is allowed only within the same file as the sealed class.
sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
object Home : Screen("home", "홈/Home", Icons.Filled.Home)
object Search : Screen("search", "검색/Search", Icons.Filled.Search)
object Settings : Screen("settings", "설정/Settings", Icons.Filled.Settings)
}
// 전체 화면 레이아웃과 하단 바를 구성하는 메인 컴포저블
// 전체 화면 레이아웃 및 네비게이션을 담당하는 메인 컴포저블
// Main composable that configures the full-screen layout and bottom bar
// Main composable that handles the full-screen layout and navigation
@Composable
fun MainScreen() {
// NavController 생성 및 기억 (Navigation 상태 관리)
// Create and remember NavController (manage navigation state)
val navController = rememberNavController()
// 하단 메뉴에 표시될 항목 리스트
// List of items to be displayed in the bottom menu
val bottomNavItems = listOf(Screen.Home, Screen.Search, Screen.Settings)
// -- 상단 앱바를 위한 설정 / Settings for the top app bar
// 현재 화면정보 가져오기 : 현재 백 스택 항목을 가져와서 현재 화면의 제목을 파악
// Get current screen information: Get the current back stack item and determine the title of the current screen.
val navBackStackEntry by navController.currentBackStackEntryAsState()
// 현재 화면정보와 리스트 네임과 일치하는지 확인 ,없다면 Home으로 설정
// Check if the current screen information matches the list name, if not, set it to Home
val currentScreen = bottomNavItems.find { it.route == navBackStackEntry?.destination?.route } ?: Screen.Home
// Scaffold는 앱 화면의 뼈대 레이아웃을 관리하고, 상단/하단 바, 콘텐츠, 패딩까지 자동으로 조정해 주는 컨테이너입니다.
// Scaffold is a container that manages the skeleton layout of the app screen and automatically adjusts the top/bottom bars, content, and padding.
Scaffold(
modifier = Modifier.fillMaxSize(),
// topBar 파라미터에 TopAppBar 컴포저블 추가
// Add TopAppBar composable to topBar parameter
topBar = {
MyTopAppBar(title = currentScreen.title)
},
bottomBar = {
// MyBottomNavigationBar 대신 NavigationBar 컴포저블을 직접 사용
// Use NavigationBar composable directly instead of MyBottomNavigationBar
MyBottomNavigationBar(
navController = navController,
items = bottomNavItems
)
}
) { innerPadding ->
// 네비게이션 호스트 (실제 화면이 바뀌는 영역) / content영역
// Navigation host (area where the actual screen changes) / content area
AppNavigation(
navController = navController,
modifier = Modifier.padding(innerPadding) // 패딩 적용 / Apply padding
)
}
}
// 상단 Navigation Bar 컴포저블
// TopAppBar 관련 API 사용 시 필요할 수 있습니다.(실험적 기능)
// Top Navigation Bar composable
// May be required when using TopAppBar-related APIs. (Experimental feature)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyTopAppBar(title: String) {
var expandedLeft by remember { mutableStateOf(false) }
var expandedRight by remember { mutableStateOf(false) }
Box {
CenterAlignedTopAppBar(
title = { Text(text = title) },
// 왼쪽 햄버거 메뉴 / Left hamburger menu
navigationIcon = {
Box {
IconButton(onClick = {
expandedLeft = true
expandedRight = false
}) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "메뉴/Menu"
)
}
DropdownMenu(
expanded = expandedLeft,
onDismissRequest = { expandedLeft = false }
) {
DropdownMenuItem(
text = { Text("메뉴1 / Menu1") },
onClick = { expandedLeft = false }
)
DropdownMenuItem(
text = { Text("메뉴2 / Menu2") },
onClick = { expandedLeft = false }
)
}
}
},
// 오른쪽 더보기 메뉴 / More menu on the right
actions = {
Box {
IconButton(onClick = {
expandedRight = true
expandedLeft = false
}) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = "더보기"
)
}
DropdownMenu(
expanded = expandedRight,
onDismissRequest = { expandedRight = false }
) {
DropdownMenuItem(
text = { Text("설정 변경 / Change settings") },
onClick = { expandedRight = false }
)
DropdownMenuItem(
text = { Text("로그아웃 / Log out") },
onClick = { expandedRight = false }
)
}
}
}
)
}
}
// 하단 Navigation Bar 컴포저블 (화면 전환 로직 포함)
// Bottom Navigation Bar composable (including screen transition logic)
@Composable
fun MyBottomNavigationBar(
navController: NavHostController,
items: List<Screen>
) {
NavigationBar {
// 현재 백 스택 항목을 State로 가져와 현재 화면의 Route를 파악
// Get the current back stack item as State and determine the Route of the current screen
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = screen.title) },
label = { Text(screen.title) },
// 현재 화면의 route와 탭 항목의 route가 같으면 선택됨
// If the route of the current screen and the route of the tab item are the same, it is selected.
selected = currentRoute == screen.route,
onClick = {
// 클릭 시 해당 경로로 이동 (화면 전환)
// Move to the corresponding path when clicked (screen transition)
navController.navigate(screen.route) {
// 백 스택에서 시작 화면을 제외한 모든 화면을 팝 (탭 전환 표준 패턴)
// Pop all screens except the start screen from the back stack (standard pattern for tab switching)
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
// 같은 탭을 다시 눌러도 다시 생성하지 않음
// Do not regenerate even if you press the same tab again
launchSingleTop = true
// 이전 상태 복원
// restore previous state
restoreState = true
}
}
)
}
}
}
// NavHost를 사용하여 경로에 따른 화면 매핑
// Mapping screens based on path using NavHost
@Composable
fun AppNavigation(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = Screen.Home.route, // 시작 화면 설정 / Start screen settings
modifier = modifier
) {
// 경로 정의 및 해당 Composable 함수 연결 / Defining a route and linking its Composable functions
composable(Screen.Home.route) { HomeScreen() }
composable(Screen.Search.route) { SearchScreen() }
composable(Screen.Settings.route) { SettingsScreen() }
}
}
// 각 스크린에 해당하는 컴포저블 함수들
// 스크린을 파일 분리 하지 않은 경우 이렇게 사용함
//@Composable
//fun HomeScreen() {
// Text("홈 화면입니다./Home Screen")
//}
//
//@Composable
//fun SearchScreen() {
// Text("검색 화면입니다./Search Screen")
//}
//
//@Composable
//fun SettingsScreen() {
// Text("설정 화면입니다./Settings Screen")
//}
@Preview(showBackground = true)
@Composable
fun MainScreenPreview() {
BottomNavigationBarTheme {
MainScreen()
}
}
// dp를 사용하기 위해 임시로 dp 확장 함수를 정의 (실제로는 androidx.compose.ui.unit.dp를 import 해야 함)
//import androidx.compose.ui.unit.dp
✔️ Screens/HomeScreen.kt
package com.freelifemakers.bottomnavigationbar.ui.screens
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun HomeScreen() {
Text("홈 화면 / Home Screen.")
}
✔️ Screens/SearchScreen.kt
package com.freelifemakers.bottomnavigationbar.ui.screens
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun SearchScreen() {
Text(text = "검색 화면 / Search Screen")
}
✔️ Screens/SettingsScreen.kt
package com.freelifemakers.bottomnavigationbar.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun SettingsScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = "설정 화면 / Settings Screen")
}
}
// 프리뷰 사용
@Preview
@Composable
fun SettingScreenPreview(){
Scaffold(
modifier = Modifier.fillMaxSize()
) { innerPadding ->
SettingsScreen(
modifier = Modifier.padding(innerPadding)
)
}
}
✔️ 스크린샷/ScreenShot