Buttons not working as Encoder

Thanks @amirgon. I think you were right with the looping through the buttons. I got it working based on the example here, removing that loop. My code now looks like:

main.py

...
BUTTON_A_PIN = const(18)
BUTTON_B_PIN = const(19)
BUTTON_C_PIN = const(23)

encoder_state = {'left': 0, 'right': 0, 'pressed': False}

def on_press_a(btn):
    if btn.pressed:
        encoder_state['left'] += 1
        print('on_press_a')


def on_press_b(btn):
    encoder_state['pressed'] = btn.pressed
    print('on_press_b')


def on_press_c(btn):
    if btn.pressed:
        encoder_state['right'] += 1
        print('on_press_c')

def button_read_constr():
    def button_read(drv, data):
        data.enc_diff = encoder_state['right'] - encoder_state['left']
        encoder_state['right'] = 0
        encoder_state['left'] = 0
        if encoder_state['pressed']:
            data.state = lv.INDEV_STATE.PRESSED
            encoder_state['pressed'] = False
        else:
            data.state = lv.INDEV_STATE.RELEASED
        # gc.collect()
        return False
    return button_read

Button(Pin(BUTTON_A_PIN), id=1, key=lv.KEY.UP, user_callback=on_press_a)
Button(Pin(BUTTON_B_PIN), id=2, key=lv.KEY.ENTER, user_callback=on_press_b)
Button(Pin(BUTTON_C_PIN), id=3, key=lv.KEY.DOWN, user_callback=on_press_c)

indev_drv = lv.indev_drv_t()
indev_drv.type = lv.INDEV_TYPE.ENCODER
indev_drv.read_cb = read_button

win_drv = indev_drv.register()

group = lv.group_create()
group.add_obj(dd)

win_drv.set_group(group)
...

utils.py

from input import DigitalInput


class Button:
    def __init__(self, pin, id, key, user_callback=None):
        self._press = False
        self._changed = False
        self._id = id
        self._key = key
        self._user_callback = user_callback
        self.setPin(pin)

    def _cb(self, pin, press):
        if self._press != press:
            print("State change [{}] {}->{} [{}]".format(pin, self._press, press, self._key))
            self._changed = True
            self._press = press
            return self._user_callback(self)

    @property
    def pressed(self):
        return self._press

    @property
    def changed(self):
        ch = self._changed
        self._changed = False
        return ch

    @property
    def id(self):
        return self._id

    @property
    def key(self):
        return self._key

    def setPin(self, pin):
        # DigitalIput : the class that monitors/debounces pin. Uses an irq internally
        # see : https://github.com/tuupola/micropython-m5stack/blob/master/firmware/lib/input.py
        # Can be replaced with an explicit irq on the pin object.
        self._input = DigitalInput(pin, self._cb)