loading...

Building a Pokedex app with Kotlin, KVision and Redux (part 2)

rjaros profile image Robert Jaros ・3 min read

Hello again :)

In the first part of this article we have designed the Redux model for our application. Everything is ready, the Pokémons are downloading, but there is nothing on the screen. We need to create a GUI.

Remember, you can find the complete application and all the sources in the kvision-examples GitHub repository.

The GUI

The user interface of our application consists of four parts:

  • A search input field.
  • An information text (visible during loading process and in case of an error).
  • A responsive grid of boxes for selected Pokémons.
  • Pagination buttons.

We can use the following skeleton of the start method, which is the entry point of our app and takes care of the GUI creation. The stateBinding method is the core part of the Redux architecture in KVision. It makes all the components inside the given container become functions of the state, automatically refreshed when the state changes.

    override fun start(state: Map<String, Any>) {
        root = Root("kvapp") {
            vPanel(alignItems = FlexAlignItems.STRETCH) {
                searchField()
                vPanel(alignItems = FlexAlignItems.STRETCH) {
                    maxWidth = 1200.px
                    textAlign = TextAlign.CENTER
                    marginLeft = auto
                    marginRight = auto
                }.stateBinding(store) { state -> // !!! Redux binding is here !!!
                    informationText(state)
                    pokemonGrid(state)
                    pagination(state)
                }
            }
        }
        store.dispatch(downloadPokemons())
    }

As you can see, we can delegate most of the work to some smaller methods (all of them are extension methods for the Container class).

The search input field

The searchField method is responsible for instantiating Text component. The event listener dispatches SetSearchString Redux action when the user enters some text into the field.

    private fun Container.searchField() {
        text {
            placeholder = "Enter pokemon name ..."
            width = 300.px
            marginLeft = auto
            marginRight = auto
            autofocus = true
            setEventListener<Text> {
                input = {
                    store.dispatch(PokeAction.SetSearchString(self.value))
                }
            }
        }
    }

The information text

The informationText method is responsible for displaying "Loading ..." label or an error message.

    private fun Container.informationText(state: Pokedex) {
        if (state.downloading) {
            div("Loading ...")
        } else if (state.errorMessage != null) {
            div(state.errorMessage)
        }
    }

The grid

First let's design the box, which will display the name and the picture of the single Pokémon. We will declare a new class inheriting from a DockPanel container and a Style object with some CSS properties. The PokeBox class is fairly simple - it has just two child components - an Image and a Div. We apply the style by using addCssClass method.

val pokeBoxStyle = Style {
    border = Border(1.px, BorderStyle.SOLID, Col.GRAY)
    width = 200.px
    height = 200.px
    margin = 10.px

    style("img") {
        marginTop = 30.px
    }

    style("div.caption") {
        textAlign = TextAlign.CENTER
        background = Background(Col.SILVER)
        width = 100.perc
    }
}

class PokeBox(pokemon: Pokemon) : DockPanel() {
    init {
        addCssClass(pokeBoxStyle)
        image(
            "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.url.substring(34,pokemon.url.length - 1)}.png",
            centered = true
        )
        add(Div(pokemon.name.capitalize(), classes = setOf("caption")), Side.DOWN)
    }
}

The pokemonGrid method is responsible for rendering the CSS grid with a single PokeBox object inside every cell. It uses the GridPanel component with an appropriate templateColumns property value to make it responsive.

    private fun Container.pokemonGrid(state: Pokedex) {
        if (!state.downloading && state.errorMessage == null) {
            gridPanel(
                templateColumns = "repeat(auto-fill, minmax(250px, 1fr))",
                justifyItems = GridJustify.CENTER
            ) {
                state.visiblePokemons.forEach {
                    add(PokeBox(it))
                }
            }
        }
    }

The pagination

The pagination is created by the following method, using ButtonGroup and Button KVision components. Both "next" and "previous" buttons are disabled when necessary and both dispatch suitable Redux actions.

    private fun Container.pagination(state: Pokedex) {
        if (!state.downloading && state.errorMessage == null) {
            hPanel(justify = FlexJustify.CENTER) {
                margin = 30.px
                buttonGroup {
                    button("<<") {
                        disabled = state.pageNumber == 0
                        onClick {
                            store.dispatch(PokeAction.PrevPage)
                        }
                    }
                    button(" ${state.pageNumber + 1} / ${state.numberOfPages} ", disabled = true)
                    button(">>") {
                        disabled = state.pageNumber == (state.numberOfPages - 1)
                        onClick {
                            store.dispatch(PokeAction.NextPage)
                        }
                    }
                }
            }
        }
    }

Final words

Our application is ready. We've created the whole GUI with about 100 lines of code (half of the original JavaScript project). KVision really gives you "the best of both worlds" - modern, expressive language, with lots of reusable components, that can be used with well known patterns and libraries like Redux.

The complete application in the GitHub repository has some small additions, not mentioned in this article (touch gestures, I18n) and is also a fully compatible Progressive Web App (PWA).

Thank you for reading this article and hope I'll catch you in the next one!

Cheers :)

Posted on by:

rjaros profile

Robert Jaros

@rjaros

Software developer and programming enthusiast from Poland. I like to reuse and integrate good stuff, made by other people and available as open source. Kind of mainstream adversary ;-)

Discussion

pic
Editor guide