Probably, yes. Hower, does Racket handle configuration changes (orientation, language, etc.), recycling list items to lower memory usage, arbitrary layouts for lists (and not just a single line of text) with diffs calculated between renders, and more ?

Android is complex, yes. Sometimes for no reason (hello God Activities). But sometimes, it has good reasons to do so.

I actually wrote one, with the bare minimum. HNActivity.kt is 81 lines and handles updating the list, querying

import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.concurrent.thread

class ListItemVH(val view: TextView) : RecyclerView.ViewHolder(view)

class HNActivity : AppCompatActivity() {
private lateinit var recycler: RecyclerView
private val adapter = object: RecyclerView.Adapter<ListItemVH>() {
private var _items = listOf<String>()
fun setItems(newItems: List<String>) {
DiffUtil.calculateDiff(object: DiffUtil.Callback() {
override fun getOldListSize(): Int = _items.size
override fun getNewListSize(): Int = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
_items[oldItemPosition] == newItems[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
_items[oldItemPosition] == newItems[newItemPosition]

}, true)
_items = newItems
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemVH {
val v = TextView(parent.context)
return ListItemVH(v)
}

override fun getItemCount(): Int = _items.size
override fun onBindViewHolder(holder: ListItemVH, position: Int) {
holder.view.text = _items[position]
}
}
private var search: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)

recycler = findViewById<RecyclerView>(R.id.fab)
recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

findViewById<EditText>(R.id.toolbar).addTextChangedListener {
search = it?.toString()?.takeIf { str -> str != null }
}
}

private fun performSearch() {
thread {
val db = SQLiteDatabase.openOrCreateDatabase("items.sqlite", null)
val query = when (search) {
null -> "SELECT text FROM table"
else -> "SELECT text FROM table WHERE text = ?%"
}
val args = listOfNotNull(search).toTypedArray()
val res = db.rawQuery(query, args)
val text = mutableListOf<String>()
while (!res.isAfterLast) {
text += res.getString(0)
}

adapter.setItems(text)
res.close()
db.close()
}
}

override fun onResume() {
super.onResume()
performSearch()
}
}

Add in another 30 lines for the Activity layout, and you've got your thing.
You've also got every single developer you'll work with hating you, and you'll hate maintaining this code. Just in those few lines, so much can go wrong (thread gets started and activity gets backgrounded? Oops, you crashed.)