Transitions

Types of transitions

Talking to user you will need to switch between different chat states. It can be done using four types of transitions:

  • State switch inside dialog. Doing so you will just show another window.

  • Start a dialog in same stack. In this case dialog will be added to task stack with empty dialog context and corresponding window will be shown instead on previously visible one

  • Start a dialog in new stack. In this case dialog will be shown in a new message and behave independently from current one.

  • Close dialog. Dialog will be removed from stack, its data erased. underlying dialog will be shown

_images/stack_transitions.png

Task stack

To deal with multiple opened dialogs aiogram_dialog has such thing as dialog stack. It allows dialogs to be opened one over another (“stacked”) so only one of them is visible.

  • Each time you start a dialog new task is added on top of a stack and new dialog context is created.

  • Each time you close a dialog, task and dialog context are removed.

You can start same dialog multiple times, and multiple contexts (identified by intent_id) will be added to stack preserving the order. So you must be careful restarting you dialogs: do not forget to clear stack or it will eat all your memory

Starting with version 1.0 you can create new stacks but default one exists always.

State switch

Simplest thing you can do to change UI layout is to switch dialog state. It does not affect task stack and just draws another window. Dialog context is kept the same, so all your data is still available.

There are several ways to do it:

  • dialog_manager.switch_to method. Pass another state and window will be switched

  • dialog_manager.next method. It will switch to the next window in the same order they were passed during dialog creation. Cannot be called when the last window is active

  • dialog_manager.back method. Switch to the opposite direction (to the previous one). Cannot be called when the first window is active

Let’s create thee windows with buttons and these transitions:

_images/switchstate.png

The code may look like:

from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery

from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const


class DialogSG(StatesGroup):
    first = State()
    second = State()
    third = State()


async def to_second(callback: CallbackQuery, button: Button,
                    manager: DialogManager):
    await manager.switch_to(DialogSG.second)


async def go_back(callback: CallbackQuery, button: Button,
                  manager: DialogManager):
    await manager.back()


async def go_next(callback: CallbackQuery, button: Button,
                  manager: DialogManager):
    await manager.next()


dialog = Dialog(
    Window(
        Const("First"),
        Button(Const("To second"), id="sec", on_click=to_second),
        state=DialogSG.first,
    ),
    Window(
        Const("Second"),
        Row(
            Button(Const("Back"), id="back2", on_click=go_back),
            Button(Const("Next"), id="next2", on_click=go_next),
        ),
        state=DialogSG.second,
    ),
    Window(
        Const("Third"),
        Button(Const("Back"), id="back3", on_click=go_back),
        state=DialogSG.third,
    )
)

It is ok to use these methods in message handler or if you have additional logic. But for simple cases it looks too complex. To simplify it we have special types of buttons. Each one can contain custom text if needed:

  • SwitchTo - calls switch_to when clicked. State is provided via constructor attribute

  • Next - calls next when clicked

  • Back - calls back when clicked

An example from above may be rewritten using these buttons:

from aiogram.filters.state import StatesGroup, State

from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Back, Next, Row, SwitchTo
from aiogram_dialog.widgets.text import Const


class DialogSG(StatesGroup):
    first = State()
    second = State()
    third = State()


dialog = Dialog(
    Window(
        Const("First"),
        SwitchTo(Const("To second"), id="sec", state=DialogSG.second),
        state=DialogSG.first,
    ),
    Window(
        Const("Second"),
        Row(
            Back(),
            Next(),
        ),
        state=DialogSG.second,
    ),
    Window(
        Const("Third"),
        Back(),
        state=DialogSG.third,
    )
)

Note

You can wonder, why we do not set an id to Back/Next buttons. Though it is normally recommended, these buttons do usually the same action so they have default id.

If you have multiple buttons of the same type in a window with on_click callback, you should explicitly set different ids.

Starting a dialog

Each dialog is like a function in python. Is has some input (start_data), output (result) and is local data (dialog_data and widget_data). When you start dialog in same stack (not passing StartMode.NEW_STACK) it is show “above” current dialog. User stop interacting with current dialog windows and sees new one from this moment.

Dialog is identified by his starting state and you need to pass it when starting a dialog. It doesn’t have to be a first state in dialog, but be careful with it. There are several ways to do it:

  • call dialog_manager.start method.

  • use Start keyboard widget, which calls same method by itself.

Started dialog will have his own empty context and has no access to parent one. You can specify its behavior passing some data to start method. It will be stored inside context and is available as dialog_manager.start_data.

You can store data in dialog_manager.dialog_data and it will be kept until it is closed and accessed only within this dialog handlers. Stateful widgets store their data is similar way and it is also not shared across opened dialogs.

You have no limitations which dialog to start. You are limited only by a depth of a stack: no more than 100 dialogs can be placed in one stack simultaneously. You can even open the same dialog multiple times and each time it will placed above and have new context.

from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery

from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Start
from aiogram_dialog.widgets.text import Const


class DialogSG(StatesGroup):
    first = State()


class SubDialogSG(StatesGroup):
    first = State()
    second = State()


async def start_subdialog(callback: CallbackQuery, button: Button,
                          manager: DialogManager):
    await manager.start(SubDialogSG.second, data={"key": "value"})


dialog = Dialog(
    Window(
        Const("Main dialog"),
        Start(Const("Start 1"), id="start", state=SubDialogSG.first),
        Button(Const("Start 2"), id="sec", on_click=start_subdialog),
        state=DialogSG.first,
    ),
)

subdialog = Dialog(
    Window(
        Const("Subdialog: first"),
        state=SubDialogSG.first,
    ),
    Window(
        Const("Subdialog: second"),
        state=SubDialogSG.second,
    ),
)

Closing a dialog

When dialog is closed it is removed from stack deleting context. From this moment user returns to a dialog which was underneath the current one.

To close a dialog you have to methods:

  • call dialog_manager.done.

  • use Cancel button.

Parent dialog has no access to the context of child one. But you can pass some data as a result to done() method and then process it in on_process_result callback of parent dialog.

from typing import Any

from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery

from aiogram_dialog import Data, Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Cancel, Start
from aiogram_dialog.widgets.text import Const


class DialogSG(StatesGroup):
    first = State()


class SubDialogSG(StatesGroup):
    first = State()


async def main_process_result(start_data: Data, result: Any,
                              dialog_manager: DialogManager):
    print("We have result:", result)


dialog = Dialog(
    Window(
        Const("Main dialog"),
        Start(Const("Start 1"), id="start", state=SubDialogSG.first),
        state=DialogSG.first,
    ),
    on_process_result=main_process_result,
)


async def close_subdialog(callback: CallbackQuery, button: Button,
                          manager: DialogManager):
    await manager.done(result={"name": "Tishka17"})


subdialog = Dialog(
    Window(
        Const("Subdialog"),
        Button(Const("Close"), id="btn", on_click=close_subdialog),
        Cancel(Const("Close")),
        state=DialogSG.first,
    ),
)