Skip to content

Basic Usage

Natan Vieira edited this page Aug 16, 2026 · 134 revisions

On this page you will learn only the basic functionality of a Inventory Framework view.

Let's get started! Extend the View class.

Preview
importme.devnatan.inventoryframework.View;
classCoolViewextendsView {
/* ...everything goes here... */
}

Customization is done through onInit(ViewConfigBuilder) which gets called once and before your view gets initialized.
Override that function and use this to change things link title, size, type and so on.

Change the title to "Octopus" with title(...)

Preview
importme.devnatan.inventoryframework.ViewConfigBuilder;
@OverridepublicvoidonInit(ViewConfigBuilderconfig) {
config.title("Octopus");
}

Specify an inventory size using size(...) or maxSize()

Preview
@OverridepublicvoidonInit(ViewConfigBuilderconfig) {
config.size(5); // rowsconfig.size(45); // full size
}
@OverridepublicvoidonInit(ViewConfigBuilderconfig) {
config.maxSize();
}

In the example above we change our inventory type to Item Hopper

Preview
importme.devnatan.inventoryframework.ViewType;
@OverridepublicvoidonInit(ViewConfigBuilderconfig) {
config.type(ViewType.HOPPER);
}

Warning

Some modifiers like size are only supported in specific inventory types. This is a Minecraft limitation.

Non supported operations are NO-OP.


Per-Player Configuration

As explained before, onInit(ViewConfigBuilder) is called once and no Player or Entity parameter is accessible. To customize your view based on the player that is opening the inventory, use onOpen(OpenContext).

This handler is called before the inventory gets created and shown to the player. With it you can:

In the example below we change the title of the inventory to the name of the player that's opening it

Use modifyConfig() to change configuration. Any option you learned earlier can be used.

Preview
importme.devnatan.inventoryframework.context.OpenContext;
@OverridepublicvoidonOpen(OpenContextopen) {
finalPlayerplayer = open.getPlayer();
open.modifyConfig()
.title("Hi, " + player.getName() + "!");
}

Cancel the context to prevent inventory from being opened

@OverridepublicvoidonOpen(OpenContextopen) {
finalPlayerplayer = open.getPlayer();
finalbooleanshouldCancel = player.getName().equals("DevNatan");
open.setCancelled(shouldCancel);
}

Rendering Items

Inventory Framework uses the onFirstRender(RenderContext) to render items.

This handler is called once while inventory is being shown to the player. With it you can:

  • Add items to the inventory
  • Access the inventory directly (not recommended)
  • Do something when the inventory is successfully shown to the player
importme.devnatan.inventoryframework.context.RenderContext;
@OverridepublicvoidonFirstRender(RenderContextrender) {
/* ... */
}

The most basic usage is the slot function

Preview
importme.devnatan.inventoryframework.context.RenderContext;
@OverridepublicvoidonFirstRender(RenderContextrender) {
render.slot(4, newItemStack(Material.EGG));
}



It is also possible to define the position of the item based on row and column.

Preview
@OverridepublicvoidonFirstRender(RenderContextrender) {
render.slot(2, 5, newItemStack(Material.EGG));
}



In case you need to add an item to the first or last slot of the inventory, using the firstSlot and lastSlot.

Preview
@OverridepublicvoidonFirstRender(RenderContextrender) {
render.firstSlot(newItemStack(Material.EGG));
render.lastSlot(newItemStack(Material.EGG));
}



Using these functions is highly recommended as this function is based on the container's settings and is self-adaptive, i.e. if you change the type or size of the container or have a conditionally defined size this function will always set the item in the right place.

Preview
@OverridepublicvoidonInit(ViewConfigBuilderconfig) {
config.type(ViewType.HOPPER);
}
@OverridepublicvoidonFirstRender(RenderContextrender) {
render.firstSlot(newItemStack(Material.EGG));
render.lastSlot(newItemStack(Material.EGG));
}

Through the availableSlot function you can resemble the behavior of the Inventory.addItem of Bukkit that sets the item in the next available slot of the inventory.

But, this function does more than just that, it adapts to your view conditions, for example: if you have a layout it sets the item in the next available slot in the layout, now, if you have pagination it sets the item in the next available slot respecting paging limits.

Preview
@OverridepublicvoidonFirstRender(RenderContextrender) {
render.firstSlot(newItemStack(Material.EGG));
render.availableSlot(newItemStack(Material.DIAMOND));
}

It is especially useful in cases where there is a series of data to be iterated and defined items from this data.

Preview
@OverridepublicvoidonFirstRender(RenderContextrender) {
// not be ignored in the iteration belowrender.slot(1, newItemStack(Material.EGG));
for (inti = 1; i <= 5; i++) {
render.availableSlot(newItemStack(Material.DIAMOND, i));
}

Filling a row or column

row/column let you fill a specific row or column, in order.

Each call fills exactly one item, into the next available slot of that row/column — it does not fill the whole row/column by itself. To fill an entire row/column, call it once per item (e.g. in a loop), the same way you would with availableSlot.

Slots are 1-indexed and filled left-to-right for rows, top-to-bottom for columns, skipping any slot that's already occupied (e.g. by a layout item). Calling it more times than the row/column can hold throws a SlotFillExceededException.

@OverridepublicvoidonFirstRender(RenderContextrender) {
// Each iteration fills ONE slot; 9 calls are needed to fill a 9-wide row.for (inti = 0; i < render.getContainer().getColumnsCount(); i++) {
render.row(1).withItem(newItemStack(Material.LIME_STAINED_GLASS_PANE));
}
}

Use firstRow()/lastRow()/firstColumn()/lastColumn() as shortcuts for the first/last row or column, so you don't need to know the container's dimensions or hardcode 1. These also fill one item per call.

@OverridepublicvoidonFirstRender(RenderContextrender) {
render.firstRow().withItem(newItemStack(Material.LIME_STAINED_GLASS_PANE));
render.lastColumn().withItem(newItemStack(Material.ORANGE_STAINED_GLASS_PANE));
}

row(n) vs row(n, factory)

Both overloads (and their column/firstRow/lastRow/firstColumn/lastColumn counterparts) still fill one item per call — the factory variant doesn't fill more slots, it just changes when the item builder is created:

  • row(1) creates the builder immediately and returns it, so you configure it inline with whatever's already in scope: render.row(1).withItem(...).onClick(...).
  • row(1, (index, builder) -> ...) defers creating the builder until the slot is actually resolved, and passes you index — the position of that call within the row/column (0, 1, 2...) — as a parameter, instead of you tracking a loop variable yourself. This is mainly handy for pulling from a list by index or reusing a method reference across calls: render.row(1, this::renderRowItem).
render.row(1, (index, builder) -> builder.withItem(items.get(index)));
render.lastColumn((index, builder) -> builder.withItem(...));

Rendering Dynamic Items

The declaration for rendering dynamic items is similar to static the only difference is that you will need to pass a render function parameter to the item instead of the item itself.

You will need dynamically rendered items when:

@OverridepublicvoidonFirstRender(RenderContextrender) {
render.slot(1, newItemStack(Material.EGG));
}

Initial Context Data

Data is defined initially through the opening function to be later retrieved within the view using initialState of the Advanced State Management feature.

In the example below we have an initial text that will be the title of the inventory as soon as the player opens it.

classMyViewextendsView {
privatefinalState<String> textState = initialState("text");
@OverridepublicvoidonOpen(OpenContextopen) {
open.modifyConfig().title(textState.get(open));
}
}

Now open the inventory using the state key in the initial opening data map.

viewFrame.open(MyView.class, player, ImmutableMap.of("text", "Hello World"));

You can use the type directly without specifying a key.

classMyViewextendsView {
privatefinalState<String> textState = initialState();
@OverridepublicvoidonOpen(OpenContextopen) {
open.modifyConfig().title(textState.get(open));
}
}
viewFrame.open(MyView.class, player, "Hello World");

Registering our Views

In order for the IF to recognize your view and for you to display it to a player, you need to register it, and for that you will need what we call a ViewFrame.

On plugin onEnable, create a new ViewFrame instance.

finalclassMyPluginextendsJavaPlugin {
@OverridepublicvoidonEnable() {
ViewFrameviewFrame = ViewFrame.create(this);
}
}

Now that you have a ViewFrame instance, you need to add your views to the ViewFrame instance, use with(...) to do it.

finalclassMyPluginextendsJavaPlugin {
@OverridepublicvoidonEnable() {
ViewFrameviewFrame = ViewFrame.create(this)
.with(newMyView());
}
}

You still don't have your views registered, ViewFrame.open will not work, now you need to call the register function to complete the operation.

finalclassMyPluginextendsJavaPlugin {
@OverridepublicvoidonEnable() {
ViewFrameviewFrame = ViewFrame.create(this)
.with(newMyView()) // add view to ViewFrame instance
.register(); // registers this ViewFrame instance
}
}

As soon as you register a ViewFrame it is available in Bukkit's ServicesManager in case you need to access it from elsewhere.

Also, there is a function called unregister in ViewFrame for dynamic unregistering, it is not necessary that you call it in onDisable the IF already does it automatically.

Opening and Closing

Now you can open your view for a player, for that, access your recently created ViewFrame instance from the Registering our Views section and call open.

viewFrame.open(MyView.class, player);

The example below opens MyView for the player every time he writes something in the chat.

finalclassMyPluginextendsJavaPluginimplementsListener {
// Move to a final value to use it laterprivatefinalViewFrameviewFrame = ViewFrame.create(this).with(newMyView());
@OverridepublicvoidonEnable() {
viewFrame.register();
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandlerpublicvoidonChat(AsyncPlayerChatEventevent) {
viewFrame.open(MyView.class, event.getPlayer());
}
}

For closing a view, there are three different functions: two are conteext-confined and one is global.

  • context.closeForPlayer(...) only closes for the player participating in an interaction, e.g. if the player clicks on an item and you use this function it will close only for the player who interacted with the item.
  • context.closeForEveryone(...) closes inventory for all viewers in that context, e.g. if the player clicks on an item and you use this function it will close for all players in that context (for shared contexts)
  • view.closeGlobally(...) closes the root, this function is not available through the context but in the scope of the view, that is, if you have access to the view instance you can access it. Closes for all contexts, all players in the server that's viewing this inventory

As soon as the player closes the view's inventory the global function onClose is called.

importme.devnatan.inventoryframework.View;
classCoolViewextendsView {
@OverridepublicvoidonClose(CloseContextclose) {
// do something on close
}
}

It is possible to prevent the inventory from being closed by canceling the context.

importme.devnatan.inventoryframework.View;
classCoolViewextendsView {
@OverridepublicvoidonClose(CloseContextclose) {
close.setCancelled(true);
}
}

Next Topics

See also about Interaction Handling to handle player interactions on your recently created items!

Welcome to the Inventory Framework documentation.

▶️ Introduction

🧩 Core Topics

💡 Built-In Features

🧰 Extra Features

🤓 Advanced Usage

⚙️ Internal Mechanisms

🛠️ Tooling

You can find practical examples in the examples directory.

Clone this wiki locally