0

I have a Fragment containing a list of strings (company names) which works with SearchView. However, due to many companies having long names, is there a way where I can type in an abbreviation for a company name rather than having to type in the whole company name? 'FTSE 150' and 'FTSE 250' are self-explanatory hence don't need abbereviations.

Abberviations for company names

  • GSK - GlaxoSmithKline plc
  • HSX - Hiscox Ltd
  • IHG - InterContinental Hotels Group plc
  • MKS - Marks & Spencer Group plc

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="company_names">
        <item>@string/glaxosmithkline_plc</item>
        <item>@string/hiscox_ltd</item>
        <item>@string/intercontinental_hotels_group_plc</item>
        <item>@string/marks_and_spencer_group_plc</item>
        <item>@string/ftse_150</item>
        <item>@string/ftse_250</item>
    </string-array>

    <string name="glaxosmithkline_plc">GlaxoSmithKline plc</string>
    <string name="hiscox_ltd">Hiscox Ltd</string>
    <string name="intercontinental_hotels_group_plc">InterContinental Hotels Group plc</string>
    <string name="marks_and_spencer_group_plc">Marks &amp; Spencer Group plc</string>
    <string name="ftse_150">FTSE 150</string>
    <string name="ftse_250">FTSE 250</string>
</resources>

fragment class

class MyFragment : androidx.fragment.app.Fragment() {

    private var mAdapter: MyListAdapter? = null

    private lateinit var mRecyclerView: androidx.recyclerview.widget.RecyclerView

    private var mTwoPane: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.layout_recyclerview, container, false)
        mTwoPane = (activity as androidx.fragment.app.FragmentActivity).findViewById<View>(R.id.detail_container) != null

        mRecyclerView = view.findViewById(R.id.recyclerView_list)
        mRecyclerView.setHasFixedSize(true)
        mRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.activity)
        mRecyclerView.addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(Objects.requireNonNull<Context>(context), LinearLayout.VERTICAL))

        val myList = ArrayList<Companies>()

//        val items = resources.getStringArray(R.array.company_names)
//        for (n in items) {
//            val company = Companies(0, "", "")
//            myList.add(company)
//        }

        val companyA = Companies(1, "GlaxoSmithKline plc", "GSK")
        val companyB = Companies(2, "Hiscox Ltd", "HSX")
        val companyC = Companies(3, "InterContinental Hotels Group plc", "IHG")
        val companyD = Companies(4, "Marks & Spencer Group plc", "MKS")
        val companyE = Companies(5, "FTSE 150", "")
        val companyF = Companies(6, "FTSE 250", "")

        val myList = DatabaseHandler(this.context!!)
        myList.insertData(companyA)
        myList.insertData(companyB)
        myList.insertData(companyC)
        myList.insertData(companyD)
        myList.insertData(companyE)
        myList.insertData(companyF)

        mAdapter = MyListAdapter(activity!!, myList, mTwoPane)

        mRecyclerView.adapter = mAdapter

        return view
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        val mInflater = Objects.requireNonNull<androidx.fragment.app.FragmentActivity>(activity).menuInflater
        mInflater.inflate(R.menu.menu_search, menu)

        val searchView = searchitem.actionView as SearchView

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String): Boolean {
                mAdapter!!.filter.filter(newText)
                return false
            }
        })

        super.onCreateOptionsMenu(menu, inflater)
    }
}

MyListAdapter class

class MyListAdapter(private val mCtx: Context, private val myList: MutableList<Companies>,
                          private val
mTwoPane: Boolean) : androidx.recyclerview.widget.RecyclerView.Adapter<MyListAdapter
.CompanyViewHolder>(), Filterable {
    private var myListFull = myList.toMutableList()

    private val companyFilter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
            val filteredList = ArrayList<Companies>()

            when {
                constraint == null || constraint.isEmpty() -> filteredList.addAll(myListFull)
                else -> {
                    val filterPattern = constraint.toString().toLowerCase().trim { it <= ' ' }

                    for (item in myListFull) {
                        when {
                            item.companyName!!.toLowerCase().contains(filterPattern) ->
                                filteredList.add(item)
                        }
                    }
                }
            }

            val results = Filter.FilterResults()
            results.values = filteredList
            return results
        }

        override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults?) {
            myList.clear()
            myList.addAll(results!!.values as List<Companies>)
            notifyDataSetChanged()
        }
    }

    inner class CompanyViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView
    .ViewHolder(itemView) {
        var tvTitle: TextView = itemView.findViewById(R.id.tv_RVItem)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CompanyViewHolder {
        val inflater = LayoutInflater.from(mCtx)
        val v = inflater.inflate(R.layout.recyclerview_item_textview, parent, false)
        return CompanyViewHolder(v)
    }

    override fun onBindViewHolder(holder: CompanyViewHolder, position: Int) {
        val product = myList[holder.adapterPosition]

        holder.tvTitle.text = product.companyfuName
    }

    override fun getItemCount(): Int {
        return myList.size
    }

    override fun getFilter(): Filter {
        return companyFilter
    }
}

enter image description here

enter image description here

enter image description here

UPDATES

Custom model class

data class Companies (val id: String, val fullName: String, val abbreviation: String)

updated Adapter class

class MyListAdapter(private val mCtx: Context,
                    private val mCompanies: MutableList<Companies>,
                    private val mTwoPane: Boolean) : androidx.recyclerview.widget.RecyclerView.Adapter<MyListAdapter
.CompanyViewHolder>(), Filterable {
    private val mCompaniesFull = mCompanies.toMutableList()

    private val companyFilter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
            val filteredList = if (constraint == null || constraint.isEmpty()) {
                mCompanies
            } else {
                val filterText = constraint.toString()
                mCompanies.filter { it.companyName.matchesIgnoreCase(filterText) || it.companyAbbreviation.matchesIgnoreCase(filterText) }
            }

            val results = Filter.FilterResults()
            results.values = filteredList
            return results
        }

        override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults?) {
            mCompanies.clear()
            mCompanies.addAll(results!!.values as List<Companies>)
            notifyDataSetChanged()
        }
    }

    private fun String.matchesIgnoreCase(otherString: String): Boolean {
        return this.toLowerCase().contains(otherString.trim().toLowerCase())
    }

    inner class CompanyViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView
    .ViewHolder(itemView) {
        var tvTitle: TextView = itemView.findViewById(R.id.tv_RVItem)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CompanyViewHolder {
        val inflater = LayoutInflater.from(mCtx)
        val v = inflater.inflate(R.layout.recyclerview_item_textview, parent, false)
        return CompanyViewHolder(v)
    }

    override fun onBindViewHolder(holder: CompanyViewHolder, position: Int) {
        val product = mCompanies[holder.adapterPosition]
        holder.tvTitle.text = product.companyName
    }

    override fun getItemCount(): Int {
        return mCompanies.size
    }

    override fun getFilter(): Filter {
        return companyFilter
    }
}

updated Fragment class

class MonFragment : androidx.fragment.app.Fragment() {

    private var mAdapter: MyListAdapter? = null

    private lateinit var mRecyclerView: androidx.recyclerview.widget.RecyclerView

    private var mTwoPane: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.layout_recyclerview, container, false)
        mTwoPane = (activity as androidx.fragment.app.FragmentActivity).findViewById<View>(R.id.detail_container) != null

        mRecyclerView = view.findViewById<RecyclerView>(R.id.recyclerView_list)
        mRecyclerView.setHasFixedSize(true)
        mRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.activity)
        mRecyclerView.addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(Objects.requireNonNull<Context>(context), LinearLayout.VERTICAL))

        mCompanies.add(Companies("GlaxoSmithKline plc", "GSK"))
        mCompanies.add(Companies("Hiscox Ltd", "HSX"))
        mCompanies.add(Companies("InterContinental Hotels Group plc", "IHG"))
        mCompanies.add(Companies("Marks & Spencer Group plc", "MKS"))
        mCompanies.add(Companies("FTSE 150", ""))
        mCompanies.add(Companies("FTSE 250", ""))

        mAdapter = MyListAdapter(activity!!, mCompanies, mTwoPane)

        mRecyclerView.adapter = mAdapter

        return view
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        val mInflater = Objects.requireNonNull<androidx.fragment.app.FragmentActivity>(activity).menuInflater
        mInflater.inflate(R.menu.menu_search, menu)

        val searchitem = menu.findItem(R.id.action_search)
        val searchView = searchitem.actionView as SearchView

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String): Boolean {
                mAdapter!!.filter.filter(newText)
                mAdapter!!.notifyDataSetChanged()

                return false
            }
        })

        super.onCreateOptionsMenu(menu, inflater)
    }
}
wbk727
  • 8,017
  • 12
  • 61
  • 125
  • please share MyListAdapter and specifically it's filter property impl. You would have to do the magic there – Jakub Licznerski Mar 24 '19 at 19:51
  • @JakubLicznerski Added :-) – wbk727 Mar 24 '19 at 20:38
  • Why don't you want to add one field with abbreviations and fill it with data? Then search for any of two fields. – CoolMind Mar 24 '19 at 21:22
  • @CoolMind I'm trying to minimise eye movement when reading the data. Not helpful particularly for longer names just to see 3 digits afterwards. Also, dozens more names will be added in future. – wbk727 Mar 24 '19 at 21:29
  • You don't have to show second field, only first. But search for both. – CoolMind Mar 24 '19 at 21:36
  • @CoolMind Do the abbreviations need to be in `strings.xml` even though they're not translatable? – wbk727 Mar 24 '19 at 21:46
  • 1
    I didn't read the code, but I think you have a database with strings. So you can read data from two fields of the database. `strings.xml` is intended for constant resources, it cannot be changed until application upgrade. – CoolMind Mar 25 '19 at 07:08
  • 1
    As @CoolMind said `strings.xml` most problably is not great solution for you, but if you need to use it [here](https://stackoverflow.com/a/4960071/6752997) is an answer to similar question. – Jakub Licznerski Mar 25 '19 at 07:25
  • @JakubLicznerski, great! I think, you should create an answer here. – CoolMind Mar 25 '19 at 08:51
  • @CoolMind I don't have any databases as I want my app to work without an internet connection. – wbk727 Mar 25 '19 at 10:50
  • Then you can create a HashMap with strings as @JakubLicznerski advised. Or an ArrayList of classes, containing id, title and abbreviation. – CoolMind Mar 25 '19 at 11:44

1 Answers1

1

Following the discussion in comments I'd suggest two approaches for storing the data:

  1. Device storage either with SharedPreferences or databases: SQLite, Room (they are lightweight local storage databases, Internet connection is not needed).
  2. Device memory (not recommended) as a List of hardcoded objects initialized either in MainActivity or Fragment's onCreate method (dependent on the usage).

As @CoolMind suggested you should implement a custom model class CompanyName(id, fullName, abbreviation) and then filter by the alternative of fullName and abbreviation.

EDIT: In MyListAdapter get rid of myListFull as it does nothing and put:

private val stockFilter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
            val filteredList = if (constraint == null || constraint.isEmpty()) myList
                else {
                   val filterText = constraint.toString()
                   myList.filter { it.fullName.matchesIgnoreCase(filterText ) || it.abbreviation.matchesIgnoreCase(filterText) }
                }
            val results = Filter.FilterResults()
            results.values = filteredList
            return results
        }
}

Also add in the MyListAdapter class this extension function:

private fun String.matchesIgnoreCase(otherString: String): Boolean {
    return this.toLowerCase().contains(otherString.trim().toLowerCase())
}
wbk727
  • 8,017
  • 12
  • 61
  • 125
Jakub Licznerski
  • 1,008
  • 1
  • 17
  • 33
  • I have tried the 1st suggestion but I still don't understand how to show inserted data in the RecyclerView. – wbk727 Mar 28 '19 at 23:22
  • Unresolved reference 'fullName' and 'abbreviation'. See my updated code. – wbk727 Mar 31 '19 at 14:21
  • Because you use a class named `Stock` in filter, in `DatabaseHandler` you have `Tasks` and `Companies` I believe it all should be `CompanyName` or better `Company` which is the suggested model class... there is big mess in the code you posted, so it's hard to guess. I've posted a solution proposition which you should adjust to your code not copy-paste. – Jakub Licznerski Mar 31 '19 at 15:32
  • What about the **Code for concern** section? Surely that code would need to change as I'm not using a string array but a database helper instead. – wbk727 Mar 31 '19 at 18:07
  • the idea is to get rid of `strings.xml` 100% and query the database instead of `resources.getStringArray(R.array.stock_names)` – Jakub Licznerski Mar 31 '19 at 18:10
  • the idea is to get rid of `strings.xml` 100% and query the database instead of `resources.getStringArray(R.array.stock_names)`. So a method `getTask` should be changed to return `Company` objects. – Jakub Licznerski Mar 31 '19 at 18:11
  • In the `getTask` method, the parentheses in `val companies = Companies()` return this error: `No value passed for parameters 'id', 'fullName' and 'abbreviation'`. What values should be passed here? – wbk727 Mar 31 '19 at 20:40
  • Look up how **data classes**, it's **properties** and **val**s work in Kotlin, Also take a look at Kotlin's **constructor syntax** – Jakub Licznerski Mar 31 '19 at 20:58
  • Ok, I'm still a bit lost with the for loop at the bottom of my question, what should be used instead of `null`? – wbk727 Mar 31 '19 at 22:14
  • This again brings the **nullable type** into consideration, but I don't think its the right approach. You should assign all the data at once in the constructor (prior saving intermediate values in **val**s). I strongly recommend you to read *thoroughly* this [reference](https://kotlinlang.org/docs/reference/basic-syntax.html) – Jakub Licznerski Apr 01 '19 at 02:48
  • Do you mean the constructor of the `fragment`? – wbk727 Apr 01 '19 at 08:48
  • no, the constructor of `Company` class in `getCompany` method... exactly where the error appears... – Jakub Licznerski Apr 01 '19 at 17:43
  • The fragment class code has been updated. I changed 'Stock' to 'Companies' for easier readability. I created the table rows in the `onCreateView` method of the `Fragment` as I want to use the same `DatabaseHandler` for future data sets. Is there a way to use those rows in the `Fragment` whilst ensuring that the data can be filtered by `fullName` and `abbreviation`? – wbk727 Apr 01 '19 at 23:41
  • I believe it's a material for another question, but I'd recommend extracting the list filtering logic which you have in `MyListAdapter` to separate file and import the function which does filtering (returns a list based on a given list) and use it when you like. Meanwhile I'd kindly ask you to mark this answer as *accepted* and add a point if it helped you. – Jakub Licznerski Apr 02 '19 at 07:54
  • @MacaronLover did you run to some other additional problems? What is the cause of an unaccept? As I've invested time in helping you here I'd be asking for an upvote too if you don't mind. – Jakub Licznerski Apr 09 '19 at 18:02
  • Unfortunately, I did - the filter works as I type in letters, but afterwards when I delete those letters, the list does not return to its original state (i.e. show the whole list). – wbk727 Apr 09 '19 at 19:35
  • thats weird, try debugging inside the filter (see if it is actually called after removing a letter) and check the contents of `constraint`. Also contents of `myList` should remain unchanged – Jakub Licznerski Apr 09 '19 at 19:55
  • `val filteredList = if (constraint == null || constraint.isEmpty()) mCompanies constraint: "p"` appears when I type in a letter, but nothing happens when I remove the letter. – wbk727 Apr 10 '19 at 16:25
  • do you use the filter in `onTextChanged` as the author of this post https://stackoverflow.com/questions/18109744/android-search-in-listview-not-working-properly? – Jakub Licznerski Apr 11 '19 at 09:37
  • I'm not using `onTextChanged`. I'm using the code that you suggested. You were saying to get rid of `myListFull`. Would I need to use `val filteredList = ArrayList()`? – wbk727 Apr 11 '19 at 11:34
  • Well then I can't tell how do you intercept the text change event because you didn't share this piece of code. I will advise you do it just like it is descibed in the post (only the part with `onTextChanged` method and apply it to your filter as it is right now) – Jakub Licznerski Apr 11 '19 at 13:33
  • I actually have. See the latest code to my question. – wbk727 Apr 14 '19 at 11:54