Itokuto
Create reactive ui in android programmatically. Get rid of the boring and layout.xml files.
Install / Use
/learn @eosobande/ItokutoREADME
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
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
node-connect
342.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
85.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
342.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
342.5kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
