What I'm trying to accomplish here, is to create an custom view of ConstraintLayout wrapping a InputTextLayout and also edittext,along with a textView.
However the setting functions aren't working when setting in fragment(DataBinding). And also with the edittext, I was hoping to try two-way binding for LiveData and Observer.
Please try to approach with Kotlin
Attrs.xml
<resources>
<declare-styleable name="ErrorCasesTextInputLayout">
<attr name="isPass" format="boolean" />
<attr name="errorCase" format="enum">
<enum name="empty" value="0"/>
<enum name="format" value="1"/>
<enum name="identical" value="2"/>
</attr>
<attr name="text" format="string" value=""/>
<attr name="hint" format="string" value=""/>
</declare-styleable>
Custom View Layout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/custom_text_input_layout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="20dp"
android:focusable="false"
android:focusableInTouchMode="true"
android:paddingBottom="2dp"
android:background="#drawable/bg_edittext"
app:hintEnabled="false"
app:boxBackgroundMode="none"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/custom_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:background="#null"
android:imeOptions="actionGo"
android:ellipsize="middle"
android:singleLine="true"
android:inputType="text"
android:textSize="15sp">
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="#+id/custom_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This field is required"
android:textSize="12sp"
android:layout_marginBottom="5dp"
android:textColor="#color/errorRed"
android:visibility="gone"
app:layout_constraintStart_toStartOf="#id/custom_text_input_layout"
app:layout_constraintTop_toBottomOf="#id/custom_text_input_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Custom View Class
class ErrorCasesTextInputLayout(context: Context, attrs: AttributeSet) :
ConstraintLayout(context, attrs) {
private var _errorCase: Int
private var _isPass: Boolean
private var _hint: String?
private var _text: String?
init {
LayoutInflater.from(context)
.inflate(R.layout.custom_error_case_text_input_layout, this, true)
attrs.let {
val attributes =
context.obtainStyledAttributes(it, R.styleable.ErrorCasesTextInputLayout)
attributes.apply {
try {
_isPass = this.getBoolean(R.styleable.ErrorCasesTextInputLayout_isPass, true)
_errorCase = this.getInteger(R.styleable.ErrorCasesTextInputLayout_errorCase, 0)
_hint = this.getString(R.styleable.ErrorCasesTextInputLayout_hint)
_text = this.getString(R.styleable.ErrorCasesTextInputLayout_text)
mSetErrorCase()
mSetPass()
mSetHint()
} finally {
recycle()
}
}
}
}
fun setErrorCase(caseType: Int) {
_isPass = false
_errorCase = caseType
invalidate()
requestLayout()
}
private fun mSetHint() {
val editText = findViewById<TextInputEditText>(R.id.custom_edit_text)
if (_hint != null ) {
editText.hint = _hint
}
}
private fun mSetPass() {
val layout = findViewById<View>(R.id.custom_text_input_layout)
if (_isPass) {
layout.setBackgroundResource(R.drawable.bg_edittext)
} else {
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
}
private fun mSetErrorCase() {
val errorText = findViewById<TextView>(R.id.custom_error_message)
val layout = findViewById<View>(R.id.custom_text_input_layout)
when (_errorCase) {
0 -> {
errorText.text = EdittextErrorCase.EMPTY.errorMessage
errorText.visibility = View.VISIBLE
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
1 -> {
errorText.text = EdittextErrorCase.FORMAT.errorMessage
errorText.visibility = View.VISIBLE
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
2 -> {
errorText.text = EdittextErrorCase.UNIDENTICAL.errorMessage
errorText.visibility = View.VISIBLE
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
}
}
fun setPass(pass: Boolean) {
_isPass = pass
invalidate()
requestLayout()
}
fun setText(text: String) {
_text = text
invalidate()
requestLayout()
}
fun setHint(hint: String) {
_hint = hint
invalidate()
requestLayout()
}
fun getCurrentErrorCase(): Int {
return _errorCase
}
#InverseBindingMethods(InverseBindingMethod(
type = ErrorCasesTextInputLayout::class,
attribute = "bind:text",
event = "bind:textAttrChanged",
method = "bind:getText")
)
class CustomEditTextBinder {
companion object {
#BindingAdapter("textAttrChanged")
#JvmStatic
fun setListener(view: ErrorCasesTextInputLayout, listener: InverseBindingListener) {
val input: TextInputEditText = view.findViewById(R.id.custom_edit_text)
input.addTextChangedListener(object : TextWatcher{
override fun afterTextChanged(p0: Editable?) {
listener.onChange()
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
#BindingAdapter("text")
#JvmStatic
fun setTextValue(view: ErrorCasesTextInputLayout, value: String?) {
if (value != view._text) view.setText(value.toString())
}
#InverseBindingAdapter(attribute = "text", event = "textAttrChanged")
#JvmStatic
fun getTextValue(view: ErrorCasesTextInputLayout): String? = view._text
}
}
}
Working Fragment
class ChangeNumberFragment : Fragment() {
lateinit var binding: FragmentChangeNumberBinding
private val viewModel by viewModels<ChangeNumberViewModel> { getVmFactory() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreate(savedInstanceState)
binding = FragmentChangeNumberBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
binding.editTextNumber.setHint("Enter New Number")
binding.editTextNumber.setPass(true)
viewModel.newNumber.observe(viewLifecycleOwner, Observer {
if (it.isNullOrEmpty()) {
binding.editTextNumber.setErrorCase(1)
} else {
Logger.i(it)
binding.editTextNumber.setPass(true)
}
})
return binding.root
}
}
Two-Way Binding with liveData
app:text="#={viewModel.newNumber}"
After a day of researching and try error, I manage to success on the two-way binding.
It was an obvious error that I didn't assign the editText view to the getText method, and with an incorrect form of null handling on setText.
class CustomEditTextBinder {
companion object {
#JvmStatic
#BindingAdapter(value = ["textAttrChanged"])
fun setListener(view: ErrorCasesTextInputLayout, listener: InverseBindingListener?) {
if (listener != null) {
view.custom_edit_text.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
listener.onChange()
}
})
}
}
#JvmStatic
#InverseBindingAdapter(attribute = "text")
fun getText(view: ErrorCasesTextInputLayout): String {
return view.custom_edit_text.text.toString()
}
#JvmStatic
#BindingAdapter("text")
fun setText(view: ErrorCasesTextInputLayout, text: String?) {
text?.let {
if (it != view.custom_edit_text.text.toString()) {
view.custom_edit_text.setText(it)
}
}
}
}
}
Hope this helps for those who are also facing the same problem.
Related
I have a list of items on a Recyclerview, and I also have a SearchView to filther those items.
Every item has a favourite button, so when you click, the item adds to favorite table.
The problem is that, when I filter something and I start clicking those buttons, odd things happens: some items dissapear from the filtered list. It doesn't happen always, only sometimes. How can I fix this?
My code:
My class:
class CoasterFragment : Fragment() {
private val myAdapter by lazy { CoasterRecyclerViewAdapter(CoasterListenerImpl(requireContext(), viewModel),requireContext()) }
private lateinit var searchView: SearchView
private var _binding: FragmentCoasterBinding? = null
private val binding get() = _binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
val recyclerView = binding.recyclerCoaster
recyclerView.adapter = myAdapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.coasters().observe(viewLifecycleOwner){myAdapter.setData(it)}
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
searchView.clearFocus()
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { myAdapter.setData(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Here is my adapter code:
class CoasterRecyclerViewAdapter( val listener: CoasterListener,
val context: Context ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
private var coasterList = emptyList<CoasterFavorito>()
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int){
holder.binding.nombre.text = coasterList[position].coaster.nombre
holder.binding.parque.text = coasterList[position].coaster.parque
holder.binding.ciudad.text = coasterList[position].coaster.ciudad
holder.binding.provincia.text = coasterList[position].coaster.provincia
holder.binding.comunidad.text = coasterList[position].coaster.comunidadAutonoma
Glide
.with(context)
.load(coasterList[position].coaster.imagen)
.centerCrop()
.into(holder.binding.imagen)
holder.binding.check.isChecked = coasterList[position].favorito
holder.binding.check.setOnClickListener{
if (coasterList[position].favorito) {
listener.delFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = false
} else {
listener.addFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = true
}
}
}
override fun getItemCount(): Int{
return coasterList.size
}
fun setData(coaster: List<CoasterFavorito>){
coasterList = coaster
notifyDataSetChanged()
}
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
The search Query:
#Query ("SELECT c.*, " + "EXISTS (SELECT * from montarse where usuario_id=:id and coaster_id = c.id) as favorito " + "FROM coasters c " + "WHERE nombre LIKE :searchQuery OR parque LIKE :searchQuery OR ciudad LIKE :searchQuery OR comunidadAutonoma LIKE :searchQuery OR provincia LIKE :searchQuery")
fun searchCoaster(id: Long, searchQuery: String): Flow<List<CoasterFavorito>>
My viewModel:
fun searchDatabase( searchQuery: String): LiveData<List<CoasterFavorito>> {
return coasterDao.searchCoaster( App.getUsuario()!!.id, searchQuery).asLiveData()
}
fun addFavorito( coasterId: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
usuarioCoasterDao.create(UsuarioCoaster(App.getUsuario()!!.id, coasterId, null, null))
}
}
}
fun coasters(): LiveData<List<CoasterFavorito>> {
return coasterDao.findAllFav(App.getUsuario()!!.id).asLiveData()
}
my XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.coaster.HomeCoasterFragment">
<androidx.appcompat.widget.SearchView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="#+id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:searchHintIcon="#null"
android:queryHint="Buscar..."
android:focusable="false"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerCoaster"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/search"
tools:listitem="#layout/coaster_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried changind the clear focus().
Also I added some if else (for example, if the searchView is Empty, then load the list from the adapter as normal. If it is not empty, use the SearchView code to filter and load the list.
I have been trying to pass some argument and get a boolean as a return for a function placed in view model class and calling that function in xml using data binding.
View model:
class ChatViewModel: ViewModel() {
val latestMessageFromFirst = MutableLiveData<String>()
private val emailOfUser = MutableLiveData<String>()
val isEmailValid = MutableLiveData<Boolean>()
fun setEmailOfUser(email: String) {
emailOfUser.value = email
}
fun setLatestMessageFromFirst(data: String) {
latestMessageFromFirst.value = data
}
fun verifyEmailAddress() {
emailOfUser.value?.let { email ->
isEmailValid.value = email.isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
}
}
}
XML:
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/btnVerifyEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_30sdp"
android:background="#drawable/stroke_button"
android:text="#string/verify"
android:onClick="#{() -> viewModel.verifyEmailAddress()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/etEmailOfUser"
app:layout_constraintWidth_percent="0.3" />
Fragment:
class UserTwoFragment : Fragment() {
private lateinit var binding: FragmentUserTwoBinding
private val viewModel: ChatViewModel by activityViewModels()
override fun onResume() {
binding.viewModel = viewModel
super.onResume()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_two, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
val view = binding.root
binding.etEmailOfUser.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
var email = ""
s?.let {
email = it.toString()
}
viewModel.setEmailOfUser(email)
viewModel.isEmailValid.observe(viewLifecycleOwner) {
binding.tvIsEmailValid.text = if (it) {
getString(R.string.valid_email_text)
} else {
getString(R.string.invalid_email_text)
}
}
binding.viewModel = viewModel
}
override fun afterTextChanged(s: Editable?) { }
})
return view
}
}
Can anyone suggest how can I achieve the result of the function(called in xml) in my fragment(activity if not using fragments)?
Now the data is fetched,but I also want the progress bar to show.I am using hilt for depedency injection.Check out my files below.I highly appreciate your feedback.I arleady have the Result class which holds the states but I am not quite sure on how to connect it with viewModel.
Api interface.kt
interface CampInterface {
#GET("programs") //includes endpoint
fun getPrograms() : Call<List<Programs>>
#GET("departments") //includes endpoint
fun getDepartments() : Call<List<Departments>>
}
viewModel.kt
#HiltViewModel
class campDataViewModel #Inject constructor(private val campDataRepository: CampDataRepository) : ViewModel() {
val programList = MutableLiveData<List<Programs>>()
val departmentList = MutableLiveData<List<Departments>>()
val errorMessage = MutableLiveData<String>()
fun getProgramData() {
val response=campDataRepository.getAllPrograms()
response.enqueue(object : Callback<List<Programs>?> {
override fun onResponse(
call: Call<List<Programs>?>,
response: Response<List<Programs>?>
) {
programList.postValue(response.body())
}
override fun onFailure(call: Call<List<Programs>?>, t: Throwable) {
errorMessage.postValue(t.message)
}
})
}
fun getDepartmentData() {
val response=campDataRepository.getAllDepartments()
response.enqueue(object : Callback<List<Departments>?> {
override fun onResponse(
call: Call<List<Departments>?>,
response: Response<List<Departments>?>
) {
departmentList.postValue(response.body())
}
override fun onFailure(call: Call<List<Departments>?>, t: Throwable) {
errorMessage.postValue(t.message)
}
})
}
}
ProgramFragment.kt
#AndroidEntryPoint
class ProgramFragment : Fragment() {
lateinit var tv_data: TextView
lateinit var pg_recyclerview: RecyclerView
#Inject
lateinit var viewModelFactory: MyViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_program, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pg_recyclerview=view.findViewById(R.id.pg_recyclerview)
// this creates a vertical layout Manager
pg_recyclerview.layoutManager = LinearLayoutManager(requireContext())
//creates an adapter
val adapter= CampdataAdapter(listOf())
pg_recyclerview.adapter=adapter
val viewModel=ViewModelProviders.of(this,viewModelFactory).get(campDataViewModel::class.java)
viewModel.programList.observe(this, Observer {
when(it.status)
Log.d(TAG, "onCreate: $it")
adapter.Programs=it //it => all items in the list
adapter.notifyDataSetChanged()
})
viewModel.errorMessage.observe(this, Observer {
})
viewModel.getProgramData()
}
}
Repository.kt
class CampDataRepository (private val campInterface: CampInterface) {
fun getAllPrograms() =campInterface.getPrograms()
fun getAllDepartments() =campInterface.getDepartments()
}
Results.kt
//for state
class Result<out T>(val status: Status, val data: T?, message: String?) {
companion object {
fun <T> success(data: T?): Result<T> {
return Result(Status.SUCCESS, data, null)
}
fun <T> loading(message: String?): Result<T> {
return Result(Status.LOADING, null, message)
}
fun <T> error(message: String?): Result<T> {
return Result(Status.ERROR, null, message)
}
}
}
enum class Status {
SUCCESS,
LOADING,
ERROR
}
fragmet_program.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.ProgramFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/pg_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
>
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="65dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_nav_menu"/>
<ProgressBar
android:id="#+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/pg_recyclerview"
tools:visibility="visible"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
program adapter.kt
class CampdataAdapter ( var Programs:List<Programs>) :RecyclerView.Adapter<CampdataAdapter.Campdataholder>() {
//viewholder class
inner class Campdataholder(itemview : View) : RecyclerView.ViewHolder(itemview){
val tvprogramname =itemview.findViewById<TextView>(R.id.tv_programname)
var tvcode =itemview.findViewById<TextView>(R.id.tv_code)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Campdataholder{
//layout to inflate to the recycler view
val view = LayoutInflater.from(parent.context).inflate(R.layout.program_item,parent,false)
return Campdataholder(view)
}
override fun onBindViewHolder(holder: Campdataholder, position: Int) {
//set values or listeners for the views
var currentprogram=Programs[position]
holder.tvprogramname.text=currentprogram.Name
holder.tvcode.text=currentprogram.Code
}
override fun getItemCount(): Int {
return Programs.size
}
}
One option is to add a new variable in ViewModel val loading = MutableLiveData<Boolean>() and then observe it in view. For your example
ViewModel
val loading = MutableLiveData<Boolean>()
fun getProgramData() {
loading.postValue(true)
val response=campDataRepository.getAllPrograms()
response.enqueue(object : Callback<List<Programs>?> {
override fun onResponse(
call: Call<List<Programs>?>,
response: Response<List<Programs>?>
) {
programList.postValue(response.body())
loading.postValue(false)
}
override fun onFailure(call: Call<List<Programs>?>, t: Throwable) {
errorMessage.postValue(t.message)
loading.postValue(false)
}
})
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.loading.observe(this, Observer {
when(it) {
true -> progressBar.visibility = View.Visible
false -> progressBar.visibility = View.Gone
})
}
Change your viewModel like this
val result = MutableLiveData<Result>()
val departmentList = MutableLiveData<List<Departments>>()
val errorMessage = MutableLiveData<String>()
fun getProgramData() {
result.postValue(Result(Status.LOADING, null, "your message"))
val response=campDataRepository.getAllPrograms()
response.enqueue(object : Callback<List<Programs>?> {
override fun onResponse(
call: Call<List<Programs>?>,
response: Response<List<Programs>?>
) {
result.postValue(Result(Status.SUCCESS, response.body(), null))
}
override fun onFailure(call: Call<List<Programs>?>, t: Throwable) {
result.postValue(Result(Status.ERROR, response.body(), null))
}
})
}
Then In Fragment
viewModel.result.observe(this, Observer {
when(result.status){
Status.SUCCESS -> {
progressBar.visibility = View.GONE
adapter.Programs=it.data //it => all items in the list
adapter.notifyDataSetChanged()
}
Status.ERROR -> {
progressBar.visibility = View.GONE
//show the error message
}
Status.LOADING -> {
progressBar.visibility = View.VISIBLE
}
}
})
I'm new with these case and I want to try to get the value of dynamic EditText(s) from RecyclerView.
And in the layout :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_marginTop="#dimen/_10sdp"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/tv_question"
style="#style/textH4"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="bottom"
android:text="Ini adalah tempat pertanyaan pertama" />
<androidx.appcompat.widget.AppCompatEditText
android:id="#+id/et_answer"
android:layout_width="match_parent"
android:layout_height="#dimen/_120sdp"
android:hint="Tulis jawaban anda"
style="#style/EdittextPrimary"
android:gravity="top"
android:background="#drawable/bg_outline_grey"
android:inputType="textMultiLine"
android:layout_marginTop="#dimen/_10sdp"/>
</LinearLayout>
The question is.. how do I get the EditText value and put it in an ArrayList ?
Newest update codes :
I add my Recycler Adapter, and tried these :
class RequestJoinAdapter(
val onTextChanged: (text:String,position:Int)->Unit
) :
RecyclerView.Adapter<RequestJoinAdapter.ViewHolder>() {
var listData: MutableList<KeamananModel> = ArrayList()
private var textValue = ""
fun insertAll(data: List<KeamananModel>) {
data.forEach {
listData.add(it)
notifyItemInserted(listData.size - 1)
}
}
fun clear() {
if (listData.isNotEmpty()) {
listData.clear()
notifyDataSetChanged()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemRequestJoinBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = listData[position]
holder.bindTo(item)
holder.binding.etAnswer.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable?) {
onTextChanged.invoke(s.toString(), position)
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
textValue = s.toString().toLowerCase()
}
})
}
override fun getItemCount() = listData.size
inner class ViewHolder(val binding: ItemRequestJoinBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindTo(item: KeamananModel) {
val context = binding.root.context
binding.tvQuestion.text = item.qt
}
}
private fun isLog(msg: String) {
Log.e("join grup:", msg)
}
}
and in my current Activity, I used to add some codes like
RequestJoinAdapter { text, position ->
val values = ArrayList<Jawaban>()
val hashSet = HashSet<Jawaban>()
values.add(Jawaban(pertanyaan_no = position+1, jawaban = text))
hashSet.addAll(values)
values.clear()
values.addAll(hashSet)
val valuest = ArrayList<JawabanKeamanan>()
val hashSets = HashSet<JawabanKeamanan>()
valuest.add(JawabanKeamanan(values))
hashSets.addAll(valuest)
valuest.clear()
valuest.addAll(hashSets)
isLog("currentResult: $valuest")
}
How do I set my latest valuest into my var listJawaban: MutableList<JawabanKeamanan> = ArrayList() without any duplicate datas inside it ? What I want is like JawabanKeamanan(jawaban_keamanan=[Jawaban(pertanyaan_no=1, jawaban=t)], [Jawaban(pertanyaan_no=2, jawaban=u)]). Thanks..
The simplest answer is to implement a TextWatcher to your EditText and have it return your data when your text is changed.
Now you may ask how can I get such data in my activty? Well it's pretty simple.
Create an interface in your adapter to communicate with your activity.
Pass your interface as a constructor parameter so when you initialize your adapter in the activity your methods are implemented.
Another solution for you is to add a button in each item of your list and call your interface method in the button's OnClickListener.
EDIT:
In the snippet below I have used a lambda function for when the text changes.
class RequestJoinAdapter(
private val onClickedItem: (ArrayList<String>)->Unit,
val onTextChanged: (text:String,position:Int)->Unit
):
RecyclerView.Adapter<RequestJoinAdapter.ViewHolder>() {
var listData: MutableList<KeamananModel> = ArrayList()
var listJawaban: MutableList<Jawaban> = ArrayList()
private var textValue = ""
fun insertAll(data: List<KeamananModel>) {
data.forEach {
listData.add(it)
notifyItemInserted(listData.size - 1)
}
}
fun clear() {
if (listData.isNotEmpty()) {
listData.clear()
notifyDataSetChanged()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemRequestJoinBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = listData[position]
holder.bindTo(item)
}
override fun getItemCount() = listData.size
inner class ViewHolder(val binding: ItemRequestJoinBinding):
RecyclerView.ViewHolder(binding.root) {
fun bindTo(item: KeamananModel) {
val context = binding.root.context
binding.tvQuestion.text = item.qt
binding.etAnswer.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable?) {
if (textValue.isNotEmpty()) {
isLog(textValue)
onTextChanged.invoke(s,absoluteAdapterPosition)
} else {
}
listJawaban.add(Jawaban(pertanyaan_no = adapterPosition + 1, jawaban = textValue))
isLog(listJawaban.toString())
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
textValue = s.toString().toLowerCase()
}
})
}
}
private fun isLog(msg: String) {
Log.e("join grup:", msg)
}
}
Pay attention to the onTextChanged.invoke() method. This lambda function can be used like an interface to communicate between your adapter and your view. It will be triggered every time your text is changed.
Finally instantiate your adapter like below:
val adapter = RequestJoinAdapter({
}, { text, position ->
//onTextChanged
})
The position argument is there to help you know which TextView was changed
In my android app Api 28 with Kotlin, I create an Interface "listOfCountry.xml", an editText for search and a recycler view that contains all the list of country as the following code:
<EditText
android:id="#+id/search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:padding="5dp"
android:layout_weight="1"
android:background="#android:color/transparent"
android:fontFamily="#font/cairo"
android:hint="Search..."
android:imeOptions="actionSearch"
android:maxLines="1"
android:singleLine="true"
android:textSize="14sp"/>
<TextView
android:id="#+id/cancelSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="#font/cairo"
android:layout_marginTop="15dp"
android:layout_marginStart="20dp"
android:text="#string/cancel_msg"
android:textSize="14sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerViewGovernmentOrSectionList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/lyt_search" />
I want to get a country from the list with the search filter, the following code is the description of my adapter :
class CountryItemAdapter(var countries: Array<AddressesData>, var mListener: OnItemClickListener) : RecyclerView.Adapter<CountryItemAdapter.ViewHolder>()
, Filterable {
private var selectedPos = -1
var searchableList: MutableList<AddressesData> = arrayListOf()
private var onNothingFound: (() -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_country, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = countries.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item: AddressesData = countries[position]
holder.tickImage.visibility = (if (selectedPos == position) View.VISIBLE else View.GONE)
holder.country.text = item.englishName.toString()
holder.itemView.setOnClickListener {
selectedPos = position
mListener.onItemClick(holder.itemView, item)
notifyDataSetChanged()
}
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val country: TextView = itemView.counrty
val tickImage: ImageView = itemView.tickGovernment
}
interface OnItemClickListener {
fun onItemClick(view: View, viewModel: AddressesData)
}
override fun getFilter(): Filter {
return object : Filter() {
private val filterResults = FilterResults()
override fun performFiltering(constraint: CharSequence?): FilterResults {
searchableList.clear()
if (constraint.isNullOrBlank()) {
searchableList.addAll(countries)
} else {
val filterPattern = constraint.toString().toLowerCase().trim { it <= ' ' }
for (item in 0..countries.size) {
if (countries[item].englishName!!.toLowerCase().contains(filterPattern)) {
searchableList.add(countries[item])
}
}}
return filterResults.also {
it.values = searchableList
}
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
if (searchableList.isNullOrEmpty())
onNothingFound?.invoke()
notifyDataSetChanged()
}
and I add the following code in my activity :
search.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
adapter.filter.filter(charSequence.toString())
}
override fun afterTextChanged(editable: Editable) {}
})
The filter didn't work, I would like to know where's the problem in my code and How can I correct it to make the filter work ?
I think you lost getItemCount method in your adapter, it must return actual size of items in adapter.
Its late but may it it will help someone,
You are filtering data in searchableList list but you have applied countries list to adapter.
Change accordingly it will work.