skip to Main Content

Disclaimer
I’ve been trying to learn to code android apps over the summer. I’ve been working through the Android Basics in Kotlin course, and I’ve recently hit the point where I felt I could branch off on my own a bit.

Context
I am trying to make a character sheet app for the recently released anime 5e d&d game. My goal is just to learn how to program apps. Presently I have a MainActivity class that manages navigation via a navdrawer, and that’s working fine. So I’ve started working on the first fragment (called AboutFragment) hosted by MainActivity. This is where the user will input things such as their name, their character’s name, etc.

Problem
At present, I am using exposed dropdown menus to allow the user to select their character’s race and subrace. The selected race should determine which subraces will be available for them to select. For instance, if the user selects the "Elf" race, they should only be able to pick between, "None", "Dark", "High", or "Wood" when choosing their subrace.

As seen in the code below, I can set up the race dropdown menu just fine. But the subrace menu is currently being set up before the user has a chance to actually select a race. So the subrace menu never changes from "Please select a race".

I am under the impression that I have to create the subrace dropdown menu in the onCreateView method, but that leaves no way for the user to determine their race before the subrace menu is created. I need to be able to change which array is displayed in the subrace menu after the user has selected a race.

Thanks in advance!!!

AboutFragment Code

package com.example.anime5echaractersheet

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels
import com.example.anime5echaractersheet.databinding.FragmentAboutBinding
import com.example.anime5echaractersheet.model.SharedViewModel

/**
 * A simple [Fragment] subclass.
 * Use the [AboutFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class AboutFragment : Fragment() {

    private val sharedViewModel: SharedViewModel by activityViewModels()

    private var _binding: FragmentAboutBinding? = null
    private val binding get() = _binding!!

    /*
    * initialize binding, set up race selection dropdown menu, inflate layout
    * */
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        _binding = FragmentAboutBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        //setup race dropdown
        val races = resources.getStringArray(R.array.races)
        val raceAdapter = ArrayAdapter(requireActivity(), R.layout.dropdown_item, races)
        binding.raceAutocompleteTextView.setAdapter(raceAdapter)
        //setup subrace dropdown
        val subraces: Array<String> = createSubraceMenu()
        val subraceAdapter = ArrayAdapter(requireActivity(), R.layout.dropdown_item, subraces)
        binding.subraceAutocompleteTextView.setAdapter(subraceAdapter)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.apply { viewModel = sharedViewModel }
        //update viewModel race & size when user makes changes
        binding.raceAutocompleteTextView.doAfterTextChanged {
            binding.viewModel?.updateRaceAndSize(binding.raceAutocompleteTextView.text.toString())
        }
    }

    /*
    * nullify _binding before destroying fragment
    * */
    override fun onDestroyView() {
        super.onDestroyView()

        _binding = null
    }

    private fun createSubraceMenu(): Array<String> {
        return when (binding.viewModel?.race?.value) {
            "Archfiend" -> resources.getStringArray(R.array.archfiend_subraces)
            "Asrai" -> resources.getStringArray(R.array.asrai_subraces)
            "Blinkbeast" -> resources.getStringArray(R.array.blinkbeast_subraces)
            "Crogoblin" -> resources.getStringArray(R.array.crogoblin_subraces)
            "Demonaga" -> resources.getStringArray(R.array.demonaga_subraces)
            "Dragonborn" -> resources.getStringArray(R.array.dragonborn_subraces)
            "Dwarf" -> resources.getStringArray(R.array.dwarf_subraces)
            "Elf" -> resources.getStringArray(R.array.elf_subraces)
            "Fairy" -> resources.getStringArray(R.array.fairy_subraces)
            "Gnome" -> resources.getStringArray(R.array.gnome_subraces)
            "Grey" -> resources.getStringArray(R.array.grey_subraces)
            "Half-Dragon" -> resources.getStringArray(R.array.half_dragon_subraces)
            "Half-Elf" -> resources.getStringArray(R.array.half_elf_subraces)
            "Half-Orc" -> resources.getStringArray(R.array.half_orc_subraces)
            "Half-Troll" -> resources.getStringArray(R.array.half_troll_subraces)
            "Halfling" -> resources.getStringArray(R.array.halfling_subraces)
            "Haud" -> resources.getStringArray(R.array.haud_subraces)
            "Human" -> resources.getStringArray(R.array.human_subraces)
            "Kodama" -> resources.getStringArray(R.array.kodama_subraces)
            "Loralai" -> resources.getStringArray(R.array.loralai_subraces)
            "Nekojin" -> resources.getStringArray(R.array.nekojin_subraces)
            "Parasite" -> resources.getStringArray(R.array.races)
            "Raceless" -> resources.getStringArray(R.array.raceless_subraces)
            "Satyr" -> resources.getStringArray(R.array.satyr_subraces)
            "Slime" -> resources.getStringArray(R.array.slime_subraces)
            "Tiefling" -> resources.getStringArray(R.array.tiefling_subraces)
            else -> arrayOf("Please select a race")
        }
    }
}

fragment_about layout

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.example.anime5echaractersheet.model.SharedViewModel" />
    </data>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            tools:context=".AboutFragment">

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.Anime5eCharacterSheet.TextInputLayout.FilledBox"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:helperText="@string/player_name_helper_text">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/player_name_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:autoSizeTextType="uniform"
                    android:hint="@string/player_name"
                    android:inputType="textCapWords" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.Anime5eCharacterSheet.TextInputLayout.FilledBox"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:helperText="@string/character_name_helper_text">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/character_name_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:autoSizeTextType="uniform"
                    android:hint="@string/character_name"
                    android:inputType="textCapWords" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_input_layout_margin"
                app:helperText="@string/race_helper_text">

                <com.google.android.material.textfield.MaterialAutoCompleteTextView
                    android:id="@+id/race_autocomplete_text_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/race"
                    android:inputType="none" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_input_layout_margin"
                app:helperText="Select a subrace from the dropdown menu">

                <com.google.android.material.textfield.MaterialAutoCompleteTextView
                    android:id="@+id/subrace_autocomplete_text_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/subrace"
                    android:inputType="none" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.Anime5eCharacterSheet.TextInputLayout.FilledBox"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:helperText="@string/size_helper_text">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/size"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:focusable="false"
                    android:hint="@string/size"
                    android:inputType="none"
                    android:longClickable="false"
                    android:text="@{viewModel.size}"
                    android:textIsSelectable="true" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.Anime5eCharacterSheet.TextInputLayout.FilledBox"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:helperText="@string/description_helper_text">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/description"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:autoSizeTextType="uniform"
                    android:hint="@string/description"
                    android:inputType="textMultiLine" />
            </com.google.android.material.textfield.TextInputLayout>

        </LinearLayout>
    </ScrollView>
</layout>

string.xml

<resources>
    <string name="app_name">Anime 5e Character Sheet</string>
    <!--fragment labels-->
    <string name="about">About</string>
    <string name="stats">Stats</string>
    <string name="start">Start</string>
    <!--textfield labels-->
    <string name="player_name">Player Name</string>
    <string name="character_name">Character Name</string>
    <string name="race">Race</string>
    <string name="description">Description</string>
    <string name="size">Size (READ ONLY)</string>
    <string name="subrace">Subrace</string>
    <!--textfield helpertext-->
    <string name="player_name_helper_text">Enter your name</string>
    <string name="character_name_helper_text">Enter your character's name</string>
    <string name="race_helper_text">Select a race from the dropdown menu</string>
    <string name="size_helper_text">Your character's size (READ ONLY)</string>
    <string name="description_helper_text">Enter a physical description of your character</string>
    <!--races for race dropdown-->
    <string-array name="races">
        <item>Archfiend</item>
        <item>Asrai</item>
        <item>Blinkbeast</item>
        <item>Crogoblin</item>
        <item>Demonaga</item>
        <item>Dragonborn</item>
        <item>Dwarf</item>
        <item>Elf</item>
        <item>Fairy</item>
        <item>Gnome</item>
        <item>Grey</item>
        <item>Half-Dragon</item>
        <item>Half-Elf</item>
        <item>Half-Orc</item>
        <item>Half-Troll</item>
        <item>Halfling</item>
        <item>Haud</item>
        <item>Human</item>
        <item>Kodama</item>
        <item>Loralai</item>
        <item>Nekojin</item>
        <item>Parasite</item>
        <item>Satyr</item>
        <item>Slime</item>
        <item>Tiefling</item>
    </string-array>
    <!--subraces for subrace dropdown-->
    <string-array name="archfiend_subraces">
        <item>Aerial</item>
        <item>None</item>
    </string-array>
    <string-array name="asrai_subraces">
        <item>Blessed</item>
        <item>None</item>
    </string-array>
    <string-array name="blinkbeast_subraces">
        <item>Multi</item>
        <item>None</item>
    </string-array>
    <string-array name="crogoblin_subraces">
        <item>None</item>
        <item>Steel</item>
    </string-array>
    <string-array name="demonaga_subraces">
        <item>Aqua</item>
        <item>Fire</item>
    </string-array>
    <string-array name="dragonborn_subraces">
        <item>Black</item>
        <item>Blue</item>
        <item>Brass</item>
        <item>Bronze</item>
        <item>Copper</item>
        <item>Gold</item>
        <item>Green</item>
        <item>Red</item>
        <item>Silver</item>
        <item>White</item>
    </string-array>
    <string-array name="dwarf_subraces">
        <item>Hill</item>
        <item>Mountain</item>
    </string-array>
    <string-array name="elf_subraces">
        <item>None</item>
        <item>Dark</item>
        <item>High</item>
        <item>Wood</item>
    </string-array>
    <string-array name="fairy_subraces">
        <item>Rock</item>
        <item>Woodland</item>
    </string-array>
    <string-array name="gnome_subraces">
        <item>None</item>
        <item>Forest</item>
        <item>Rock</item>
    </string-array>
    <string-array name="grey_subraces">
        <item>Elite</item>
        <item>None</item>
    </string-array>
    <string-array name="half_dragon_subraces">
        <item>Bronze</item>
        <item>Copper</item>
        <item>Gold</item>
        <item>Silver</item>
    </string-array>
    <string-array name="half_elf_subraces">
        <item>None</item>
    </string-array>
    <string-array name="half_orc_subraces">
        <item>None</item>
    </string-array>
    <string-array name="half_troll_subraces">
        <item>None</item>
        <item>Water</item>
    </string-array>
    <string-array name="halfling_subraces">
        <item>None</item>
        <item>Lightfoot</item>
        <item>Stout</item>
    </string-array>
    <string-array name="haud_subraces">
        <item>Leaping</item>
        <item>None</item>
    </string-array>
    <string-array name="human_subraces">
        <item>Standard</item>
        <item>Variant</item>
    </string-array>
    <string-array name="kodama_subraces">
        <item>None</item>
        <item>Void</item>
    </string-array>
    <string-array name="loralai_subraces">
        <item>None</item>
        <item>Paired</item>
    </string-array>
    <string-array name="nekojin_subraces">
        <item>None</item>
        <item>Panthera</item>
    </string-array>
    <string-array name="raceless_subraces">
        <item>None</item>
    </string-array>
    <string-array name="satyr_subraces">
        <item>Forest</item>
        <item>Mountain</item>
        <item>Under</item>
    </string-array>
    <string-array name="slime_subraces">
        <item>Blue</item>
        <item>Other</item>
    </string-array>
    <string-array name="tiefling_subraces">
        <item>Bloodline of Asmodeus</item>
        <item>None</item>
    </string-array>
</resources>

2

Answers


  1. Chosen as BEST ANSWER

    Update

    A custom array adapter was, in fact, the correct thing to implement. I simply created a new adapter called SubraceAdapter which could take in in a reference to the 'race' LiveData from my sharedViewModel. I created a method inside of the SubraceAdapter that would clear the dropdown menu, check the LiveData, and then add in the correct subrace options. This method is called in onViewCreated each time raceAutoCompleteTextView.doAfterTextChanged is triggered. My solution code is below:

    AboutFragment

    package com.example.anime5echaractersheet
    
    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.ArrayAdapter
    import androidx.core.widget.doAfterTextChanged
    import androidx.fragment.app.Fragment
    import androidx.fragment.app.activityViewModels
    import com.example.anime5echaractersheet.adapter.SubraceAdapter
    import com.example.anime5echaractersheet.databinding.FragmentAboutBinding
    import com.example.anime5echaractersheet.model.SharedViewModel
    
    /**
     * A simple [Fragment] subclass.
     * Use the [AboutFragment.newInstance] factory method to
     * create an instance of this fragment.
     */
    class AboutFragment : Fragment() {
    
        private lateinit var subraceAdapter: SubraceAdapter
    
        private val sharedViewModel: SharedViewModel by activityViewModels()
    
        private var _binding: FragmentAboutBinding? = null
        private val binding get() = _binding!!
    
        /*
        * initialize binding, set up race selection dropdown menu, inflate layout
        * */
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
    
            _binding = FragmentAboutBinding.inflate(inflater, container, false)
            binding.lifecycleOwner = viewLifecycleOwner
            //setup race dropdown
            val races = resources.getStringArray(R.array.races)
            val raceAdapter = ArrayAdapter(requireActivity(), R.layout.dropdown_item, races)
            binding.raceAutocompleteTextView.setAdapter(raceAdapter)
            //setup subrace dropdown
            subraceAdapter = SubraceAdapter(requireActivity(), R.layout.dropdown_item, sharedViewModel.race)
            binding.subraceAutocompleteTextView.setAdapter(subraceAdapter)
    
            return binding.root
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            binding.apply { viewModel = sharedViewModel }
            //update viewModel race & size when user makes changes
            binding.raceAutocompleteTextView.doAfterTextChanged {
                binding.viewModel?.updateRaceAndSize(binding.raceAutocompleteTextView.text.toString())
                subraceAdapter.updateList()
            }
        }
    
        /*
        * nullify _binding before destroying fragment
        * */
        override fun onDestroyView() {
            super.onDestroyView()
    
            _binding = null
        }
    }
    

    SubraceAdapter

    package com.example.anime5echaractersheet.adapter
    
    import android.content.Context
    import android.widget.ArrayAdapter
    import androidx.lifecycle.LiveData
    import com.example.anime5echaractersheet.R.array.*
    
    class SubraceAdapter(context: Context, resource: Int, private val race: LiveData<String>) :
        ArrayAdapter<String?>(context, resource) {
    
        fun updateList() {
            clear()
            val newSubraceList: Array<String?> = when (race.value) {
                context.resources.getStringArray(races)[0] -> {
                    context.resources.getStringArray(archfiend_subraces)
                }
                context.resources.getStringArray(races)[1] -> {
                    context.resources.getStringArray(asrai_subraces)
                }
                context.resources.getStringArray(races)[2] -> {
                    context.resources.getStringArray(blinkbeast_subraces)
                }
                context.resources.getStringArray(races)[3] -> {
                    context.resources.getStringArray(crogoblin_subraces)
                }
                context.resources.getStringArray(races)[4] -> {
                    context.resources.getStringArray(demonaga_subraces)
                }
                context.resources.getStringArray(races)[5] -> {
                    context.resources.getStringArray(dragonborn_subraces)
                }
                context.resources.getStringArray(races)[6] -> {
                    context.resources.getStringArray(dwarf_subraces)
                }
                context.resources.getStringArray(races)[7] -> {
                    context.resources.getStringArray(elf_subraces)
                }
                context.resources.getStringArray(races)[8] -> {
                    context.resources.getStringArray(fairy_subraces)
                }
                context.resources.getStringArray(races)[9] -> {
                    context.resources.getStringArray(gnome_subraces)
                }
                context.resources.getStringArray(races)[10] -> {
                    context.resources.getStringArray(grey_subraces)
                }
                context.resources.getStringArray(races)[11] -> {
                    context.resources.getStringArray(half_dragon_subraces)
                }
                context.resources.getStringArray(races)[12] -> {
                    context.resources.getStringArray(half_elf_subraces)
                }
                context.resources.getStringArray(races)[13] -> {
                    context.resources.getStringArray(half_orc_subraces)
                }
                context.resources.getStringArray(races)[14] -> {
                    context.resources.getStringArray(half_troll_subraces)
                }
                context.resources.getStringArray(races)[15] -> {
                    context.resources.getStringArray(halfling_subraces)
                }
                context.resources.getStringArray(races)[16] -> {
                    context.resources.getStringArray(haud_subraces)
                }
                context.resources.getStringArray(races)[17] -> {
                    context.resources.getStringArray(human_subraces)
                }
                context.resources.getStringArray(races)[18] -> {
                    context.resources.getStringArray(kodama_subraces)
                }
                context.resources.getStringArray(races)[19] -> {
                    context.resources.getStringArray(loralai_subraces)
                }
                context.resources.getStringArray(races)[20] -> {
                    context.resources.getStringArray(nekojin_subraces)
                }
                context.resources.getStringArray(races)[21] -> {
                    context.resources.getStringArray(races)
                }
                context.resources.getStringArray(races)[22] -> {
                    context.resources.getStringArray(raceless_subraces)
                }
                context.resources.getStringArray(races)[23] -> {
                    context.resources.getStringArray(satyr_subraces)
                }
                context.resources.getStringArray(races)[24] -> {
                    context.resources.getStringArray(slime_subraces)
                }
                context.resources.getStringArray(races)[25] -> {
                    context.resources.getStringArray(tiefling_subraces)
                }
                else -> arrayOf("Please select a race")
            }
            addAll(newSubraceList.toList())
        }
    }
    

  2. craete a custom array adapter for autoCompleteTextView

    class ShopAddressProvinceAdapter(
    context: Context,
    resource: Int,
    ) :
    ArrayAdapter<ShopAddressProvince>(context, resource) {
    
    lateinit var binding: ItemDropdownBinding
    var list: MutableList<ShopAddressProvince> = mutableListOf()
    
    private var onItemClicked: ((id: Int, name: String) -> Unit) =
        { id, name -> }
    
    fun setOnClickListener(listener: (id: Int, name: String) -> Unit) {
        onItemClicked = listener
    }
    
    private class ProvinceVH constructor(private val binding: ItemDropdownBinding) :
        RecyclerView.ViewHolder(binding.root) {
    
        fun bind(item: ShopAddressProvince) {
            binding.apply {
                textView.text = item.text
            }
        }
    
    }
    
    @SuppressLint("ViewHolder")
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    
        val holder: ProvinceVH
    
        if (convertView == null) {
            binding = DataBindingUtil.inflate(
                LayoutInflater.from(context), R.layout.item_dropdown, parent, false
            )
            holder = ProvinceVH(binding)
            holder.itemView.tag = holder
        } else {
            holder = convertView.tag as ProvinceVH
        }
    
        getItem(position)?.let {
            holder.bind(it)
            holder.itemView.setOnClickListener { view ->
                onItemClicked.invoke(it.id, it.text)
            }
        }
    
        return holder.itemView
    }
    
    override fun getFilter(): Filter {
        return filter
    }
    
    
    
    override fun getItem(position: Int): ShopAddressProvince? {
        return list[position]
    }
    
    override fun getCount(): Int {
        return list.size
    }
    
    private val filter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val result = FilterResults()
            val suggestions: MutableList<ShopAddressProvince> = mutableListOf()
    
            if (constraint != null) {
                suggestions.clear()
                val filterPattern = constraint.toString().lowercase()
                for (item in list) {
                    if (item.text.lowercase().contains(filterPattern)) {
                        suggestions.add(item)
                    }
                }
                result.values = suggestions
                result.count = suggestions.size
            }
    
            return result
        }
    
        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            clear()
            if (results != null && results.count > 0) {
                addAll(results.values as MutableList<ShopAddressProvince>)
            } else {
                addAll(list)
            }
            notifyDataSetChanged()
        }
    
        override fun convertResultToString(resultValue: Any?): CharSequence {
            return (resultValue as ShopAddressProvince).text
        }
    
    }
    
    fun addList(data: MutableList<ShopAddressProvince>) {
        this.list = data
        notifyDataSetChanged()
    }
    

    }

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search