SkillAgentSearch skills...

Itokuto

Create reactive ui in android programmatically. Get rid of the boring and layout.xml files.

Install / Use

/learn @eosobande/Itokuto
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Description

Create reactive ui in android programmatically. Get rid of the boring and layout.xml files.

Installation

Add the dependency to your build.gradle

    dependencies {
        implementation 'com.eosobande:itokuto:1.0.0'
    }

Sample Application

Itokuto sample application

How to use

1. Layout: create a new class and implement the Widget interface

    class HomeLayout(override val context: Context) : Widget {

        override val widget: Widget
            get() =
                RStack(context)
                    .invoke {

                        +Text(context, "Welcome to Itokuto!")
                            .params(
                                RStackParams()
                                    .centerInParent()
                                    .margin(10.dp)
                            )

                    }

    }

The widget val property ideally should be a computed property since we do not need a reference for the instance.

We get a relative layout with width and height of match_parent, and a centered text view with width and height of wrap_content and 10 dp margin on all sides.

The + before the Text object means to add the the text as a child of the RStack. Only stacks (view groups) have this function, - is the opposite and removes the widget

2. Merge: create a new class and implement the Merge interface


    class HomeFrames(override val context: Context) : Merge {

        override val widgets: Array<Widget>
            get() = arrayOf(

                Frame(context)
                    .background(Color.BLUE)
                    .backgroundTint(0x33FFFFFF.colorState) // extension to convert color int to color state list
                    .id(R.id.first_frame)
                    .params(CoordinatorParams(MATCH, MATCH, behave = true))
                    .padding(bottom = 10.dp),

                Frame(context)
                    .background(Color.GREEN)
                    .id(R.id.second_frame)
                    .params(CoordinatorParams(MATCH, MATCH, behave = true))
                    .padding(bottom = 10.dp),

                Frame(context)
                    .background(Color.YELLOW)
                    .id(R.id.third_frame)
                    .params(CoordinatorParams(MATCH, MATCH, behave = true))
                    .padding(bottom = 10.dp)

            )

    }

Equivalent of a merge tag of 3 FrameLayouts, each with a background color of blue, green, and yellow respectively, and also having individual ids and padding bottom of 10 dp.

CoordinatorParams(MATCH, MATCH, behave = true) is a coordinator layout param, with match_parent on both width and height, behave = true adds the app scrolling behaviour to each frame, placing it below the app bar.

3. To include a merge object into an existing layout

    class HomeLayout(override val context: AppCompatActivity) : Widget {

        override val widget: Widget
            get() =
                Coordinator(context)
                    .invoke {

                        +AppBar(context)
                             .params(CoordinatorParams(MATCH))
                             .invoke {

                                 +ToolBar(context)
                                     .supportActionBar(this@HomeLayout.context)
                                     .minHeight(60.dp)
                                     .params(AppBarParams(MATCH, 60.dp)) // width of match_parent and height of 60 dp
                                     .elevation(3f.dp) // 3 dp elevation
                                     .background(drawable(R.drawable.round_bottom_corners)) // drawable() helper method fetching a drawable resource
                                     .navigationIcon(drawable(R.drawable.ic_upload))

                             }

                        +HomeFrames(context)

                    }

    }

Here we have a coordinator layout as our top level view, with an app bar layout containing a toolbar. And we then include the frames in a merge object from No. 2 above

4. Reactivity, states, and binding

    class MyButton(override val context: Context) : Widget {

        private val text: State<String> = State("Enabled")
        private val buttonState: State<Boolean> = State(true)

        override val widget: Button
            get() =
                Button(context, text) // text is string value of the button, button text value updates automatically when text state value changes
                    .bind(buttonState) {
                        enable(it) // enable or disable the button whenever buttonState value changes
                        text(if (it) "Enabled" else "Disabled") // change the value of text state object
                    }
                    .onClick {
                        buttonState(!buttonState()) // toggle the boolean value of buttonState
                    }

    }

This button is bound to two state objects of String and Boolean types. Whenever the text state object changes, the button text automatically updates, when the buttonState boolean changes, we toggle the enabled state of the button and update the text object

5. Recyclerview, adapters, and view holders


    // contact data item

    data class Contact(
        val firstName: String,
        val lastName: String,
        val phoneNumber: String
    )

    // contact layout item

    class ContactItem(override val context: Context) : RecyclerAdapter.Item<Contact>() {

        override val widget: Widget
            get() =
                VStack(context) // VStack is vertical stack (column / vertical linear layout)
                    .background(
                        Rectangle()
                            .solidColor(Color.GREEN)
                            .rounded()
                            .ripple()
                            .rippleColor(Color.WHITE) // green rounded rectangle bg with white ripple
                    )
                    .params(LStackParams(MATCH)) // match_parent width
                    .padding(15.dp) // 15 dp padding
                    .invoke {

                        +Text(context)
                            .bind(data) { // data is the contact state object in the parent class
                                // it = value of data state object which is of Contact type
                                text("${it.firstName} ${it.lastName}") // whenever the object changes, update the name text view
                            }
                            .params(LStackParams(MATCH))

                        +Text(context)
                            .params(LStackParams().margin(top = 5.dp))
                            .bind(data) {
                                text(it.phoneNumber) // update the phone number text view
                            }

                    }

    }

    // contact adapter
    // You can use the pre-written custom adapter or create yours
    // this is an example of Itokuto's packaged recycler adapter

    class ContactsAdapter : RecyclerAdapter<Contact, ContactsAdapter.WidgetHolder>() {

        // supports filtering
        override fun onFilter(filter: String, data: Contact) =
            data.firstName.contains(filter, true) ||
            data.lastName.contains(filter, true) ||
            data.phoneNumber.contains(filter, true)

        // makes use of AsyncListDiffer

        override fun areItemsTheSame(old: Contact, new: Contact) = old == new

        override fun areContentsTheSame(old: Contact, new: Contact) = old == new

        // on item click

        override fun onItemClick(holder: Item, data: Contact) {
            // do something with the clicked contact object
            // you also have access to the widget/view holder instance
        }

        // create the widget holder / view holder here

        override fun createWidgetHolder(context: Context, viewType: Int) = WidgetHolder(ContactItem(context))

        // the widget holder / view holder class

        class WidgetHolder(item: ContactItem) : BaseItem<Contact, ContactItem>(item)

    }

    // the recycler page layout

    class ContactsListPage(override val context: Context) : Widget {

        private val adapter: ContactsAdapter = ContactsAdapter().apply {

            // adding items to the adapter

            add(listOf(
                Contact("Emmanuel", "Sobande", "+123456789"),
                Contact("Michael", "Sobande", "+2356789022"),
                Contact("John", "Doe", "+03447204248247"),
                Contact("Jane", "Smith", "+53893936404"),
            ))

            // available methods
            // clear, add one, add one at position, add list, add list at position,
            // replace one, replace list, remove, refresh, and filter(string)

        }

        override val widget: Button
            get() =
                Recycler(context)
                    .setItemViewCacheSize(20)
                    .view { // helper method to access the view directly
                        // this scope for the underlying recyclerview object
                        layoutManager = LinearLayoutManager(context)
                        // we're accessing the layoutManager property directly
                    }
                    .adapter(adapter)

    }

6. Activities & Fragments: setContentView and onCreateView


    class HomeActivity : AppCompatActivity(), PageStructure {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(HomeLayout(this)) // extension method
        }
    
    }

    class HomeFragment : Fragment() {

        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? = view ?: HomeLayout

Related Skills

View on GitHub
GitHub Stars15
CategoryDevelopment
Updated6mo ago
Forks4

Languages

Kotlin

Security Score

67/100

Audited on Sep 29, 2025

No findings