Implementing a Custom Navigation Bar Using MDNavigationBar - Challenges and Limitations

69 views
Skip to first unread message

Yassine Ouchen

unread,
Jan 16, 2025, 11:45:52 AMJan 16
to Kivy users support
I'm building a custom navigation bar for my app using MDNavigationBar and MDNavigationItem from KivyMD. The goal is to create a reusable component for navigation, styled to fit my app’s theme, and ensure that the navigation bar appears only on specific screens (e.g., Home, Budget, and Statistics) while being excluded from others like Signup and Login.  

########################
Here is my custom navigation bar
customnavigationbar.py:
from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
from kivy.properties import StringProperty

class BaseNavigationItem(MDNavigationItem):
    icon = StringProperty()
    text = StringProperty()
   
class CustomNavigationBar(MDNavigationBar):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.theme_cls.primary_palette = "Steelblue"

customnavigationbar.kv:
<BaseNavigationItem>
    MDNavigationItemIcon:
        icon: root.icon
        theme_icon_color: 'Custom'
        icon_color_normal: 'gray'
        icon_color_active: '#33658A'
       
    MDNavigationItemLabel:
        theme_text_color: 'Custom'
        text: root.text
        text_color_normal: 'gray'
        text_color_active: '#33658A'
        theme_font_name: 'Custom'
        font_name: 'assets/fonts/Inter-Bold.ttf' if root.active else 'assets/fonts/Inter-Regular.ttf'
        theme_font_size: 'Custom'
        font_size: '12sp'
       
<CustomNavigationBar>
   
    BaseNavigationItem:
        active: True
        icon: "home" if self.active else "home-outline"
        text: "Home"
       
    BaseNavigationItem:
        icon: "file-document" if self.active else "file-document-outline"
        text: "Budget"
       
    BaseNavigationItem:
        icon: "chart-box" if self.active else "chart-box-outline"
        text: "Statistics"
       
    BaseNavigationItem:
        icon: "account-settings" if self.active else "account-settings-outline"
        text: "Settings"
########################

Here is the main.py and main.kv before implementing the custom nav bar to my app
########################
main.py
from kivy.config import Config
Config.set('graphics', 'width', '375')
Config.set('graphics', 'height', '665')
from kivymd.app import MDApp

from app.screens.signupscreen.signupscreen import SignupScreen
from app.screens.loginscreen.loginscreen import LoginScreen
from app.screens.homescreen.homescreen import HomeScreen


class MainApp(MDApp):
    def build(self):
        self.title = "Expenses Tracker"
       
    def on_start(self):
        self.root.current = 'home_screen'
       
if __name__ == "__main__":
    MainApp().run()

main.kv
#: include app/screens/signupscreen/signupscreen.kv
#: include app/screens/loginscreen/loginscreen.kv
#: include app/screens/homescreen/homescreen.kv

ScreenManager:
    id: screen_manager
       
    SignupScreen:
        id: signup_screen
        name: 'signup_screen'
       
    LoginScreen:
        id: login_screen
        name: 'login_screen'
       
    HomeScreen:
        name: 'home_screen'
#######################

Approaches I Tried:

First Method: Add the ScreenManager and CustomNavigationBar inside a MDBoxLayout or MDFloatLayout in main.kv:

#: include app/screens/signupscreen/signupscreen.kv
#: include app/screens/loginscreen/loginscreen.kv
#: include app/screens/homescreen/homescreen.kv

#: include app/customwidgets/customnavigationbar/customnavigationbar.kv

MDBoxLayout: #or MDFloatLayout
    size: root.width, root.height
    orientation: 'vertical'
    md_bg_color: 'white'
   
    ScreenManager:
        id: screen_manager
           
        SignupScreen:
            id: signup_screen
            name: 'signup_screen'
           
        LoginScreen:
            id: login_screen
            name: 'login_screen'
           
        HomeScreen:
            name: 'home_screen'
           
    CustomNavigationBar:
        id: navigation_bar  
        #pos_hint: {'center_x': .5, 'y': 0} when using MDFloatLayout

Issues of this method:

  1. Using MDBoxLayout: The height of screens is reduced to accommodate the navigation bar at the bottom, making widgets on the screens (e.g., Home) appear smaller.
  2. Using MDFloatLayout: Requires manually leaving space at the bottom of each screen to avoid overlapping with the navigation bar. 
  3. The navigation bar is always visible, even on screens like Signup and Login. Hiding it by reducing opacity is a workaround but not ideal.

Second Method: Add the CustomNavigationBar in each screen's Kivy file individually.

Example: homescreen.kv:

<HomeScreen>
    navigation_bar: navigation_bar
    size: root.width, root.height
    md_bg_color: '#ffffff'
   
    CustomNavigationBar:
        id: navigation_bar  
        pos_hint: {'center_x': .5, 'y': 0}

Issues of this method:

  1. Navigation bar elements (like active states of icons) must be updated dynamically when navigating between screens.
  2. Works better only with NoTransition in ScreenManager.
If you are in a similar situation what would you do?


Radosław Głębicki

unread,
Jan 16, 2025, 11:22:27 PMJan 16
to Kivy users support
remove_widget() cannot work in that case?

Yassine Ouchen

unread,
Jan 17, 2025, 11:41:57 AMJan 17
to Kivy users support

Thank you for your suggestion to use the add_widget/remove_widget method for managing the navigation bar visibility. While this method can work in some scenarios, it introduces a critical limitation: the loss of state. Let me explain this using a specific example:

In my App:

  • Home Screen and Statistics Screen both will have the navigation bar.
  • Both screens include a FAB button that navigates to an Add Transaction Screen, which does not contain the navigation bar.
  • If we navigate from the Statistics Screen to the Add Transaction Screen, we need to remove the navigation bar using remove_widget.
  • When returning to the Statistics Screen, we need to re-add the navigation bar using add_widget.
  • After re-adding the navigation bar, the active tab will reset to its default state (e.g., "Home").
  • To restore the correct active state, we would need to write additional logic to manually update the active state of the navigation bar's MDNavigationItem. (we need to:  Track which tab was active before removing the navigation bar, and Restore the active state of the correct tab when re-adding the navigation bar. ). 
The solution that worked well for me was to toggle the visibility of the navigation bar instead of removing and adding it. 
Here's how I implemented it:  

In main.py, I added a method to show or hide the navigation bar:  

'''
def toggle_nav_bar(self, show):
    nav_bar = self.root.ids.navigation_bar
    if show:
        nav_bar.opacity = 1
        nav_bar.disabled = False
        nav_bar.pos_hint = {'center_x': .5, 'y': 0}
    else:
        nav_bar.opacity = 0
        nav_bar.disabled = True
        nav_bar.pos_hint = {'center_x': .5, 'top': 0} # 'top: 0' hides the navigation bar in the bottom of the screen.
'''

Then, inside the on_pre_enter() method of each screen, I call:  

'''
MDApp.get_running_app().toggle_nav_bar(True)  # Or False for screens without a navigation bar
'''

In some cases, it isn't necessary to explicitly call toggle_nav_bar() on every screen. For example, in the Signup screen, I don't call toggle_nav_bar() because the only way to access this screen is from the Login screen, where the navigation bar is already hidden using toggle_nav_bar(False)

Do you think this is a good approach? Are there any potential drawbacks I might not have considered?

Also, I’m facing an issue with changing the background color of the MDNavigationBar. Since it inherits from ThemableBehavior, I tried setting theme_bg_color = 'Custom' and md_bg_color = 'color', but the result doesn’t look good—the color appears distorted or unexpected. I suspect it might be because MDNavigationBar uses an image as its background. Do you know why this might be happening or how to fix it?

ElliotG

unread,
Jan 17, 2025, 7:00:22 PMJan 17
to Kivy users support
Another approach to consider is nesting ScreenManagers.  In the example below the root widget is a ScreenManager, the home screen, login screen and nav_screens are under this Screenmanager.  The third screen, 'nav_screens' contains another Screenmanager and a NavigationBar that works with that ScreenManager.

Here is an example:

from kivy.lang import Builder
from kivy.properties import StringProperty

from kivymd.app import MDApp

from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
from kivymd.uix.screen import MDScreen


class BaseMDNavigationItem(MDNavigationItem):

    icon = StringProperty()
    text = StringProperty()


class BaseScreen(MDScreen):
    ...


KV = '''
<BaseMDNavigationItem>

    MDNavigationItemIcon:
        icon: root.icon

    MDNavigationItemLabel:
        text: root.text

<BaseScreen>
    MDLabel:
        text: root.name
        halign: "center"
       
<MDLabelScreen@MDScreen>:
    MDBoxLayout:
        orientation: "vertical"
        md_bg_color: self.theme_cls.backgroundColor
        MDLabel:
            text: root.name
            halign: 'center'
        Button:
            size_hint_y: None
            height: dp(48)
            text: 'Next'
            on_release: root.manager.current = root.manager.next()
       
<ScreenSubSection@MDScreen>:
    MDBoxLayout:
        orientation: "vertical"
        md_bg_color: self.theme_cls.backgroundColor
   
        MDScreenManager:
            id: screen_manager
   
            BaseScreen:
                name: "Screen 1"
   
            BaseScreen:
                name: "Screen 2"
   
        MDNavigationBar:
            on_switch_tabs: app.on_switch_tabs(*args)
   
            BaseMDNavigationItem
                icon: "gmail"
                text: "Screen 1"
                active: True
   
            BaseMDNavigationItem
                icon: "twitter"
                text: "Screen 2"

MDScreenManager:
    MDLabelScreen:
        name: 'home'
    MDLabelScreen:
        name: 'login'
    ScreenSubSection:
        name: 'nav_screens'
'''


class Example(MDApp):
    def on_switch_tabs(
            self,
            bar: MDNavigationBar,
            item: MDNavigationItem,
            item_icon: str,
            item_text: str,
    ):
        self.root.get_screen('nav_screens').ids.screen_manager.current = item_text

    def build(self):
        return Builder.load_string(KV)


Example().run()
Message has been deleted
Message has been deleted

Yassine Ouchen

unread,
Jan 19, 2025, 10:04:03 AMJan 19
to Kivy users support

Hi ElliotG ,

I just wanted to say thank you for suggesting the nested ScreenManager approach for managing the navigation bar in my app. I implemented it, and it worked beautifully! It saved me a lot of effort compared to my previous method of toggling the navigation bar's visibility and initializing it in on_pre_enter() for each screen.

That said, this method adds a little complexity when navigating from a specific screen within the nested ScreenManager (e.g., home_screen) to a major screen like transaction_screen in the root ScreenManager, and vice versa.

here is the structure of main.kv:

#: include app/screens/signupscreen/signupscreen.kv
#: include app/screens/loginscreen/loginscreen.kv  

#: include app/screens/navigationscreens/navigationscreens.kv
#: include app/screens/transactionscreen/transactionscreen.kv

ScreenManager:
    id: screen_manager
   
    LoginScreen:
        name: 'login_screen'
       
    SignupScreen:
        name: 'signup_screen'
       
    NavigationScreens:
        name: 'nav_screens'
       
    TransactionScreen:
        name: 'transaction_screen'


here is navigationscreens.py:

from kivymd.uix.screen import MDScreen
from kivy.properties import ObjectProperty
from app.screens.homescreen.homescreen import HomeScreen
from app.screens.statisticsscreen.statisticsscreen import StatisticsScreen

class NavigationScreens(MDScreen):
    nested_screen_manager = ObjectProperty()
    pass


Here is navigationscreens.kv:

#: include app/screens/homescreen/homescreen.kv  
#: include app/screens/statisticsscreen/statisticsscreen.kv  
#: include app/customwidgets/customnavigationbar/customnavigationbar.kv

<NavigationScreens>
    nested_screen_manager: nested_screen_manager
    size: root.width, root.height
    md_bg_color: 'white'
   
    ScreenManager:
        id: nested_screen_manager
       
        HomeScreen:
            name: 'home_screen'
           
        StatisticsScreen:
            name: 'statistics_screen'

    CustomNavigationBar:
        id: nested_navigation_bar


        pos_hint: {'center_x': .5, 'y': 0}

        on_switch_tabs: app.on_nested_switch_tabs(*args)


To navigate from home screen to transactions screen, here is what i did:

In homescreen.py:

def nav_to_add_transaction(self):
        root_manager = self.get_root_manager()
        root_manager.get_screen('transaction_screen').previous_screen = 'home_screen'
        root_manager.transition.direction = 'left'
        root_manager.current = 'transaction_screen'

    def get_root_manager(self):
        parent = self.parent.parent.parent # or using  App.get_running_app()
        return parent


To navigate back from transaction screen to home screen, I did:

class TransactionScreen(MDScreen):
    previous_screen = StringProperty('')
   
    def nav_back(self):
        root_manager = self.manager
        # we need first to navigate to  nav_screens then navigate to transaction screen
        root_manager.current = 'nav_screens'
        nested_screen_manager = root_manager.get_screen('nav_screens').nested_screen_manager
        nested_screen_manager.current = self.previous_screen

Because we can navigate to transaction screen from either home screen or statistics screen, the same logic will be applied to statistics screen.

ELLIOT GARBUS

unread,
Jan 19, 2025, 10:39:00 AMJan 19
to kivy-...@googlegroups.com
Looks good to me.  I'd recommend you use the App.get_running_app, rather than:
parent = self.parent.parent.parent # or using  App.get_running_app()

I think you'll find it to be more robust if you need to make changes in the future.


From: kivy-...@googlegroups.com <kivy-...@googlegroups.com> on behalf of Yassine Ouchen <ouchen.yt....@gmail.com>
Sent: Sunday, January 19, 2025 8:04 AM
To: Kivy users support <kivy-...@googlegroups.com>
Subject: [kivy-users] Re: Implementing a Custom Navigation Bar Using MDNavigationBar - Challenges and Limitations
 
--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/kivy-users/a928a1c0-1429-47e2-972c-720864f6b838n%40googlegroups.com.

Yassine Ouchen

unread,
Jan 19, 2025, 2:18:27 PMJan 19
to Kivy users support
Yes, it looks good. Thank you
Reply all
Reply to author
Forward
0 new messages