Recently, I have made a fork of Arduino LiquidCrystal (HD44780 display driver library) that allows the library to work with the IO abstraction library, meaning you can configure a display to use Arduino pins, an i2c 8574 IO expander or shift registers by simply changing one line of code in your sketch.
There are two additional examples provided with this version that show how to use the fork with both a shift register and an 8574 i2c IO expander. If the library is configured without an abstraction being provided, it defaults to Arduino pins, just like it used to do.
Usage is exactly the same as the standard version of the library with a minor caveat. You must call the liquid crystal begin method from within the setup() method, which is not mandatory in the regular version, but must be done in this version. Other than this very minor detail, the library should work without any other changes.
This library also uses TaskManger, making it completely compatible with the TaskManager framework, it ensures that any significant waits for the display hardware are done through task manager, to avoid potentially 100s of micros of latency.
As per the diagram above, but use a shift register instead of the i2c device.