Changed navigation to a Compose-based architecture using named routes instead of fragment IDs, and refactored all related code for both Kiwix and custom apps.

This commit is contained in:
MohitMaliFtechiz 2025-07-20 21:20:27 +05:30
parent 86d9b1266f
commit a3d6ea49f9
10 changed files with 199 additions and 161 deletions

View File

@ -18,10 +18,8 @@
package org.kiwix.kiwixmobile.main
import androidx.annotation.IdRes
data class BottomNavItem(
@IdRes val id: Int,
val route: String,
val title: String,
val iconRes: Int
)

View File

@ -61,6 +61,7 @@ import org.kiwix.kiwixmobile.core.main.ZIM_FILE_URI_KEY
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.kiwixActivityComponent
import org.kiwix.kiwixmobile.ui.KiwixDestination
import javax.inject.Inject
const val ACTION_GET_CONTENT = "GET_CONTENT"
@ -71,7 +72,7 @@ const val KIWIX_BOTTOM_BAR_ANIMATION_DURATION = 250L
class KiwixMainActivity : CoreMainActivity() {
private var actionMode: ActionMode? = null
override val cachedComponent by lazy { kiwixActivityComponent }
override val searchFragmentResId: Int = R.id.searchFragment
override val searchFragmentRoute: String = KiwixDestination.Search.route
// override val drawerContainerLayout: DrawerLayout by lazy {
// // activityKiwixMainBinding.navigationContainer
@ -90,12 +91,12 @@ class KiwixMainActivity : CoreMainActivity() {
override val mainActivity: AppCompatActivity by lazy { this }
override val appName: String by lazy { getString(R.string.app_name) }
override val bookmarksFragmentResId: Int = R.id.bookmarksFragment
override val settingsFragmentResId: Int = R.id.kiwixSettingsFragment
override val historyFragmentResId: Int = R.id.historyFragment
override val notesFragmentResId: Int = R.id.notesFragment
override val readerFragmentResId: Int = R.id.readerFragment
override val helpFragmentResId: Int = R.id.helpFragment
override val bookmarksFragmentRoute: String = KiwixDestination.Bookmarks.route
override val settingsFragmentRoute: String = KiwixDestination.Settings.route
override val historyFragmentRoute: String = KiwixDestination.History.route
override val notesFragmentRoute: String = KiwixDestination.Notes.route
override val readerFragmentRoute: String = KiwixDestination.Reader.route
override val helpFragmentRoute: String = KiwixDestination.Help.route
override val topLevelDestinations =
setOf(R.id.downloadsFragment, R.id.libraryFragment, R.id.readerFragment)
private val isBottomBarVisible = mutableStateOf(true)
@ -117,9 +118,8 @@ class KiwixMainActivity : CoreMainActivity() {
navController = rememberNavController()
KiwixMainActivityScreen(
navController = navController,
topLevelDestinations = topLevelDestinations.toList(),
isBottomBarVisible = isBottomBarVisible.value,
leftDrawerContent = rightNavigationDrawerMenuItems,
leftDrawerContent = leftNavigationDrawerMenuItems,
rightDrawerContent = { }
)
LaunchedEffect(navController) {
@ -365,7 +365,7 @@ class KiwixMainActivity : CoreMainActivity() {
private fun openZimHostFragment() {
disableDrawer()
navigate(R.id.zimHostFragment)
navigate(KiwixDestination.ZimHost.route)
}
override fun getIconResId() = mipmap.ic_launcher

View File

@ -47,19 +47,20 @@ import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import org.kiwix.kiwixmobile.R.drawable
import org.kiwix.kiwixmobile.R.id
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.main.DrawerMenuGroup
import org.kiwix.kiwixmobile.core.main.LeftDrawerMenu
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NAVIGATION_DRAWER_WIDTH
import org.kiwix.kiwixmobile.ui.KiwixDestination
import org.kiwix.kiwixmobile.ui.KiwixNavGraph
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KiwixMainActivityScreen(
navController: NavHostController,
topLevelDestinations: List<Int>,
leftDrawerContent: List<DrawerMenuGroup>,
rightDrawerContent: @Composable ColumnScope.() -> Unit,
isBottomBarVisible: Boolean = true
@ -70,26 +71,20 @@ fun KiwixMainActivityScreen(
KiwixTheme {
ModalNavigationDrawer(
drawerContent = {
Column(
Modifier
.fillMaxHeight()
.width(NAVIGATION_DRAWER_WIDTH)
) {
Column(modifier = Modifier.fillMaxSize()) {
LeftDrawerMenu(leftDrawerContent)
}
},
gesturesEnabled = true
}
) {
Box {
Scaffold(
bottomBar = {
if (isBottomBarVisible) {
BottomNavigationBar(
navController = navController,
scrollBehavior = scrollingBehavior,
topLevelDestinations = topLevelDestinations
)
}
// if (isBottomBarVisible) {
BottomNavigationBar(
navController = navController,
scrollBehavior = scrollingBehavior
)
// }
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
@ -119,45 +114,45 @@ fun KiwixMainActivityScreen(
@Composable
fun BottomNavigationBar(
navController: NavHostController,
scrollBehavior: BottomAppBarScrollBehavior,
topLevelDestinations: List<Int>
scrollBehavior: BottomAppBarScrollBehavior
) {
val bottomNavItems = listOf(
BottomNavItem(
id = id.readerFragment,
route = KiwixDestination.Reader.route,
title = stringResource(id = R.string.reader),
iconRes = drawable.ic_reader_navigation_white_24px
),
BottomNavItem(
id = id.libraryFragment,
route = KiwixDestination.Library.route,
title = stringResource(id = R.string.library),
iconRes = drawable.ic_library_navigation_white_24dp
),
BottomNavItem(
id = id.downloadsFragment,
route = KiwixDestination.Downloads.route,
title = stringResource(id = R.string.download),
iconRes = drawable.ic_download_navigation_white_24dp
)
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestinationId = navBackStackEntry?.destination?.id
if (currentDestinationId in topLevelDestinations) {
BottomAppBar(scrollBehavior = scrollBehavior) {
bottomNavItems.forEach { item ->
NavigationBarItem(
selected = currentDestinationId == item.id,
onClick = { navController.navigate(item.id) },
icon = {
Icon(
painter = painterResource(id = item.iconRes),
contentDescription = item.title
)
},
label = { Text(item.title) }
)
}
val currentDestinationRoute = navBackStackEntry?.destination?.route
BottomAppBar(
containerColor = Black,
contentColor = White,
scrollBehavior = scrollBehavior
) {
bottomNavItems.forEach { item ->
NavigationBarItem(
selected = currentDestinationRoute == item.route,
onClick = { navController.navigate(item.route) },
icon = {
Icon(
painter = painterResource(id = item.iconRes),
contentDescription = item.title
)
},
label = { Text(item.title) }
)
}
}
}

View File

@ -55,10 +55,10 @@ fun KiwixNavGraph(
) {
NavHost(
navController = navController,
startDestination = "readerFragment",
startDestination = KiwixDestination.Reader.route,
modifier = modifier
) {
composable("readerFragment") {
composable(KiwixDestination.Reader.route) {
FragmentContainer {
KiwixReaderFragment().apply {
arguments = Bundle().apply {
@ -71,7 +71,7 @@ fun KiwixNavGraph(
}
}
}
composable("libraryFragment") {
composable(KiwixDestination.Library.route) {
FragmentContainer {
LocalLibraryFragment().apply {
arguments = Bundle().apply {
@ -80,57 +80,57 @@ fun KiwixNavGraph(
}
}
}
composable("downloadsFragment") {
composable(KiwixDestination.Downloads.route) {
FragmentContainer {
OnlineLibraryFragment()
}
}
composable("bookmarksFragment") {
composable(KiwixDestination.Bookmarks.route) {
FragmentContainer {
BookmarksFragment()
}
}
composable("notesFragment") {
composable(KiwixDestination.Notes.route) {
FragmentContainer {
NotesFragment()
}
}
composable("introFragment") {
composable(KiwixDestination.Intro.route) {
FragmentContainer {
IntroFragment()
}
}
composable("historyFragment") {
composable(KiwixDestination.History.route) {
FragmentContainer {
HistoryFragment()
}
}
composable("languageFragment") {
composable(KiwixDestination.Language.route) {
FragmentContainer {
LanguageFragment()
}
}
composable("zimHostFragment") {
composable(KiwixDestination.ZimHost.route) {
FragmentContainer {
ZimHostFragment()
}
}
composable("helpFragment") {
composable(KiwixDestination.Help.route) {
FragmentContainer {
KiwixHelpFragment()
}
}
composable("kiwixSettingsFragment") {
composable(KiwixDestination.Settings.route) {
FragmentContainer {
KiwixSettingsFragment()
}
}
composable("searchFragment") {
composable(KiwixDestination.Search.route) {
FragmentContainer {
SearchFragment()
}
}
composable("localFileTransferFragment") {
composable(KiwixDestination.LocalFileTransfer.route) {
FragmentContainer {
LocalFileTransferFragment().apply {
arguments = Bundle().apply {
@ -168,3 +168,19 @@ fun FragmentContainer(
}
)
}
sealed class KiwixDestination(val route: String) {
object Reader : KiwixDestination("readerFragment")
object Library : KiwixDestination("libraryFragment")
object Downloads : KiwixDestination("downloadsFragment")
object Bookmarks : KiwixDestination("bookmarksFragment")
object Notes : KiwixDestination("notesFragment")
object Intro : KiwixDestination("introFragment")
object History : KiwixDestination("historyFragment")
object Language : KiwixDestination("languageFragment")
object ZimHost : KiwixDestination("zimHostFragment")
object Help : KiwixDestination("helpFragment")
object Settings : KiwixDestination("kiwixSettingsFragment")
object Search : KiwixDestination("searchFragment")
object LocalFileTransfer : KiwixDestination("localFileTransferFragment")
}

View File

@ -75,7 +75,7 @@ const val ACTION_NEW_TAB = "NEW_TAB"
const val NEW_TAB_SHORTCUT_ID = "new_tab_shortcut"
abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
abstract val searchFragmentResId: Int
abstract val searchFragmentRoute: String
@Inject lateinit var alertDialogShower: AlertDialogShower
@ -95,11 +95,11 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
// abstract val drawerContainerLayout: DrawerLayout
// abstract val drawerNavView: NavigationView
// abstract val readerTableOfContentsDrawer: NavigationView
abstract val bookmarksFragmentResId: Int
abstract val settingsFragmentResId: Int
abstract val historyFragmentResId: Int
abstract val notesFragmentResId: Int
abstract val helpFragmentResId: Int
abstract val bookmarksFragmentRoute: String
abstract val settingsFragmentRoute: String
abstract val historyFragmentRoute: String
abstract val notesFragmentRoute: String
abstract val helpFragmentRoute: String
abstract val cachedComponent: CoreActivityComponent
abstract val topLevelDestinations: Set<Int>
@ -324,7 +324,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
}
protected fun openHelpFragment() {
navigate(helpFragmentResId)
navigate(helpFragmentRoute)
handleDrawerOnNavigation()
}
@ -362,9 +362,9 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
}
activeFragments().filterIsInstance<FragmentActivityExtensions>().forEach {
if (it.onBackPressed(this@CoreMainActivity) == ShouldCall) {
if (navController.currentDestination?.id?.equals(readerFragmentResId) == true &&
if (navController.currentDestination?.route?.equals(readerFragmentRoute) == true &&
navController.previousBackStackEntry?.destination
?.id?.equals(searchFragmentResId) == false
?.route?.equals(searchFragmentRoute) == false
) {
drawerToggle = null
finish()
@ -400,8 +400,8 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
}
}
fun navigate(fragmentId: Int) {
navController.navigate(fragmentId)
fun navigate(route: String) {
navController.navigate(route)
}
fun navigate(fragmentId: Int, bundle: Bundle) {
@ -414,11 +414,11 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
private fun openSettings() {
handleDrawerOnNavigation()
navigate(settingsFragmentResId)
navigate(settingsFragmentRoute)
}
private fun openHistory() {
navigate(historyFragmentResId)
navigate(historyFragmentRoute)
}
fun openSearch(
@ -426,23 +426,23 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
isOpenedFromTabView: Boolean = false,
isVoice: Boolean = false
) {
navigate(
searchFragmentResId,
bundleOf(
NAV_ARG_SEARCH_STRING to searchString,
TAG_FROM_TAB_SWITCHER to isOpenedFromTabView,
EXTRA_IS_WIDGET_VOICE to isVoice
)
)
// navigate(
// searchFragmentRoute,
// bundleOf(
// NAV_ARG_SEARCH_STRING to searchString,
// TAG_FROM_TAB_SWITCHER to isOpenedFromTabView,
// EXTRA_IS_WIDGET_VOICE to isVoice
// )
// )
}
fun openZimFromFilePath(path: String) {
navigate(
readerFragmentResId,
bundleOf(
ZIM_FILE_URI_KEY to path,
)
)
// navigate(
// readerFragmentRoute,
// bundleOf(
// ZIM_FILE_URI_KEY to path,
// )
// )
}
fun openPage(
@ -456,26 +456,26 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
}
val navOptions = NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(readerFragmentResId, inclusive = true)
.setPopUpTo(readerFragmentRoute, inclusive = true)
.build()
navigate(
readerFragmentResId,
bundleOf(
PAGE_URL_KEY to pageUrl,
ZIM_FILE_URI_KEY to zimFileUri,
SHOULD_OPEN_IN_NEW_TAB to shouldOpenInNewTab
),
navOptions
)
// navigate(
// readerFragmentRoute,
// bundleOf(
// PAGE_URL_KEY to pageUrl,
// ZIM_FILE_URI_KEY to zimFileUri,
// SHOULD_OPEN_IN_NEW_TAB to shouldOpenInNewTab
// ),
// navOptions
// )
}
private fun openBookmarks() {
navigate(bookmarksFragmentResId)
navigate(bookmarksFragmentRoute)
handleDrawerOnNavigation()
}
private fun openNotes() {
navigate(notesFragmentResId)
navigate(notesFragmentRoute)
}
protected fun handleDrawerOnNavigation() {
@ -488,51 +488,57 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
}
fun findInPage(searchString: String) {
navigate(readerFragmentResId, bundleOf(FIND_IN_PAGE_SEARCH_STRING to searchString))
// navigate(readerFragmentRoute, bundleOf(FIND_IN_PAGE_SEARCH_STRING to searchString))
}
private val bookRelatedDrawerGroup = DrawerMenuGroup(
listOfNotNull(
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.bookmarks),
iconRes = R.drawable.ic_bookmark_black_24dp,
true,
onClick = { openBookmarks() }
),
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.history),
iconRes = R.drawable.ic_history_24px,
true,
onClick = { openHistory() }
),
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.pref_notes),
iconRes = R.drawable.ic_add_note,
true,
onClick = { openNotes() }
),
zimHostDrawerMenuItem
)
)
private val settingDrawerGroup = DrawerMenuGroup(
listOf(
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.menu_settings),
iconRes = R.drawable.ic_settings_24px,
true,
onClick = { openSettings() }
private val bookRelatedDrawerGroup by lazy {
DrawerMenuGroup(
listOfNotNull(
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.bookmarks),
iconRes = R.drawable.ic_bookmark_black_24dp,
true,
onClick = { openBookmarks() }
),
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.history),
iconRes = R.drawable.ic_history_24px,
true,
onClick = { openHistory() }
),
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.pref_notes),
iconRes = R.drawable.ic_add_note,
true,
onClick = { openNotes() }
),
zimHostDrawerMenuItem
)
)
)
}
open val helpAndSupportDrawerGroup = DrawerMenuGroup(
listOfNotNull(
helpDrawerMenuItem,
supportDrawerMenuItem,
aboutAppDrawerMenuItem
private val settingDrawerGroup by lazy {
DrawerMenuGroup(
listOf(
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.menu_settings),
iconRes = R.drawable.ic_settings_24px,
true,
onClick = { openSettings() }
)
)
)
)
}
private val helpAndSupportDrawerGroup by lazy {
DrawerMenuGroup(
listOfNotNull(
helpDrawerMenuItem,
supportDrawerMenuItem,
aboutAppDrawerMenuItem
)
)
}
/**
* Returns the "Wi-Fi Hotspot" menu item in the left drawer.
@ -563,14 +569,16 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
*/
abstract val aboutAppDrawerMenuItem: DrawerMenuItem?
val rightNavigationDrawerMenuItems = listOf<DrawerMenuGroup>(
bookRelatedDrawerGroup,
settingDrawerGroup,
helpAndSupportDrawerGroup
)
val leftNavigationDrawerMenuItems by lazy {
listOf<DrawerMenuGroup>(
bookRelatedDrawerGroup,
settingDrawerGroup,
helpAndSupportDrawerGroup
)
}
protected abstract fun getIconResId(): Int
abstract val readerFragmentResId: Int
abstract val readerFragmentRoute: String
abstract fun createApplicationShortcuts()
abstract fun setDialogHostToActivity(alertDialogShower: AlertDialogShower)

View File

@ -20,10 +20,13 @@ package org.kiwix.kiwixmobile.core.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Surface
@ -33,8 +36,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.components.ONE
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray350
import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray600
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NAVIGATION_DRAWER_WIDTH
@ -43,7 +49,8 @@ fun LeftDrawerMenu(drawerMenuGroupList: List<DrawerMenuGroup>) {
Surface(
modifier = Modifier
.width(NAVIGATION_DRAWER_WIDTH)
.fillMaxHeight(),
.fillMaxHeight()
.statusBarsPadding(),
shadowElevation = EIGHT_DP
) {
Column {
@ -64,9 +71,22 @@ fun LeftDrawerMenu(drawerMenuGroupList: List<DrawerMenuGroup>) {
@Composable
private fun DrawerGroup(items: List<DrawerMenuItem>) {
Column {
items.filter { it.visible }.forEach { item ->
// Add a horizontal divider at end if there are items in a group.
val dividerColor = if (isSystemInDarkTheme()) {
MineShaftGray600
} else {
MineShaftGray350
}
val visibleMenuItems = items.filter { it.visible }
if (visibleMenuItems.size == ONE) {
HorizontalDivider(color = dividerColor)
}
visibleMenuItems.forEach { item ->
DrawerMenuItemView(item)
}
if (visibleMenuItems.size == ONE) {
HorizontalDivider(color = dividerColor)
}
}
}
@ -84,6 +104,6 @@ private fun DrawerMenuItemView(item: DrawerMenuItem) {
},
modifier = Modifier
.fillMaxWidth()
.clickable { item.onClick }
.clickable { item.onClick.invoke() }
)
}

View File

@ -1535,7 +1535,7 @@ abstract class CoreReaderFragment :
private fun goToBookmarks(): Boolean {
val parentActivity = requireActivity() as CoreMainActivity
parentActivity.navigate(parentActivity.bookmarksFragmentResId)
parentActivity.navigate(parentActivity.bookmarksFragmentRoute)
return true
}

View File

@ -225,8 +225,8 @@ class SearchFragment : BaseFragment() {
}
private fun goBack() {
val readerFragmentResId = (activity as CoreMainActivity).readerFragmentResId
findNavController().popBackStack(readerFragmentResId, false)
val readerFragmentRoute = (activity as CoreMainActivity).readerFragmentRoute
findNavController().popBackStack(readerFragmentRoute, false)
}
private fun getSearchListItemForQuery(query: String): SearchListItem? =

View File

@ -35,11 +35,12 @@ data class OpenSearchItem(
private val openInNewTab: Boolean = false
) : SideEffect<Unit> {
override fun invokeWith(activity: AppCompatActivity) {
val readerFragmentResId = (activity as CoreMainActivity).readerFragmentResId
activity.navigate(
readerFragmentResId,
bundleOf(SEARCH_ITEM_TITLE_KEY to SEARCH_ITEM_TITLE_KEY)
)
val readerFragmentRoute = (activity as CoreMainActivity).readerFragmentRoute
// TODO fix this when properly migrated to compose navigation.
// activity.navigate(
// readerFragmentRoute,
// bundleOf(SEARCH_ITEM_TITLE_KEY to SEARCH_ITEM_TITLE_KEY)
// )
activity.setNavigationResultOnCurrent(
SearchItemToOpen(
searchListItem.value,

View File

@ -187,7 +187,7 @@ abstract class CoreSettingsFragment : SettingsContract.View, BaseFragment() {
(activity as CoreMainActivity?)?.let {
it.navController.apply {
popBackStack()
navigate(it.settingsFragmentResId)
navigate(it.settingsFragmentRoute)
}
}
}