I have a main activity with a heading and a search field (edit text), I want to be able to search and the results are immediately shown in the fragment, like an onChange instead of waiting for the user to click a button to filter results. (which is in the activity).
I can get it working if I include the Edit Text in my fragment too, but I don't want it that way for design purposes, I'd like to retrieve the user values as they are typed from the activity, and get them in my fragment to filter results
I've tried Bundles but could not get it working, and also not sure If i could use Bundles to get the results as they are being input.
Here's a screenshot to help understand better
You can make it happen using ViewModel + MVVM architecture.
MainActivity:
binding.editText.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) {
}
override fun afterTextChanged(s: Editable?) {
viewModel.updateSearchText(s)
}
})
ViewModel:
private val _searchText = MutableLiveData<Editable?>()
val searchText: LiveData<Editable?> get() = _searchText
fun updateSearchText(text: Editable?) {
_searchText.value = s
}
Fragment:
searchText.observe(viewLifecycleOwner) {
// TODO: handle the searched query using [it] keyword.
}
If you don't know what View Model is or how to implement it, use the official Google tutorial: https://developer.android.com/codelabs/basic-android-kotlin-training-viewmodel
Another way to achieve this (besides using an Android ViewModel) is use the Fragment Result API.
For instance, if you place the EditText into a fragment (let's call it QueryFragment), you can get the result of the QueryFragment in your SearchResults fragment like so:
// In QueryFragment
editText.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) { }
override fun afterTextChanged(s: Editable?) {
setFragmentResult("searchQueryRequestKey", bundleOf("searchQuery" to s.toString()))
}
})
// In SearchResultsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Retrieve the searched string from QueryFragment
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("searchQueryRequestKey") { requestKey, bundle ->
val searchQuery = bundle.getString("searchQuery")
// Perform the search using the searchQuery and display the search results
}
}
Related
I am using this library to put a carousel view in an Android app: https://github.com/ImaginativeShohag/Why-Not-Image-Carousel
I'm also trying to use the showcase type, but a prerequisite to use this type is creating a custom layout for the carousel items.
Creating the layout I understand, but the OP uses this example in Kotlin to show how the custom layout is actually used:
binding.carousel3.carouselListener = object : CarouselListener {
override fun onCreateViewHolder(
layoutInflater: LayoutInflater,
parent: ViewGroup
): ViewBinding? {
return ItemCustomFixedSizeLayout1Binding.inflate(layoutInflater, parent, false)
}
override fun onBindViewHolder(
binding: ViewBinding,
item: CarouselItem,
position: Int
) {
val currentBinding = binding as ItemCustomFixedSizeLayout1Binding
currentBinding.imageView.apply {
scaleType = imageScaleType
// carousel_default_placeholder is the default placeholder comes with
// the library.
setImage(item, R.drawable.carousel_default_placeholder)
}
}
}
val listThree = mutableListOf<CarouselItem>()
for (item in DataSet.three) {
listThree.add(
CarouselItem(
imageUrl = item.first,
caption = item.second
)
)
}
binding.carousel3.setData(listThree)
binding.customCaption.isSelected = true
binding.carousel3.onScrollListener = object : CarouselOnScrollListener {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int,
position: Int,
carouselItem: CarouselItem?
) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
carouselItem?.apply {
binding.customCaption.text = caption
}
}
}
override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int,
position: Int,
carouselItem: CarouselItem?
) {
// ...
}
}
// Custom navigation
binding.btnGotoPrevious.setOnClickListener {
binding.carousel3.previous()
}
binding.btnGotoNext.setOnClickListener {
binding.carousel3.next()
}
I'm having some trouble figuring out what exactly this code is doing and how it would look in Java. Any pointers would be greatly appreciated!
A Quick Guess
It seems that the listener is providing the callback of Recycler View. If you need me to guess within a second, and I will say the custom view is a Recycler View using listener to allow users to register the Recycler View methods (which is the Adapter using in the RV)
Deep Investigation
1st question: What is the Custom View class for id=carousel3 in KotlinActivity in the sample project
Ans: org.imaginativeworld.whynotimagecarousel.ImageCarousel.
(P.S. identical between activity_kotlin.xml and activity_test.xml)
(Below is a screen cap, don't try to click the links since it will not work :))
Let's got to search ImageCarousel and we will find ImageCarousel.kt. Let's find CarouselListener in there
We can see that when CarouselListener is set, it will immediately assign to adapter?.listener (Just ignore the "?" sign if you are not familiar with Kotlin)
2nd question: What is adapter here?
Ans from the same file:
private var adapter: FiniteCarouselAdapter? = null
3rd question: What is FiniteCarouselAdapter?
Ans: Its a RecyclerView.Adapter
open class FiniteCarouselAdapter(
...
) : RecyclerView.Adapter<FiniteCarouselAdapter.MyViewHolder>() {
Last question: How is it related to FiniteCarouselAdapter#listener/CarouselListener/adapter?.listener?
When the RecyclerView#Adapter requires to call the ViewHolder method, it will call to CarouselListener methods instead.
In FiniteCarouselAdapter:
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
...
// Init listeners
listener?.onBindViewHolder(
holder.binding,
item,
realItemPosition
)
As CarouselListener is an interface, the method implementation will be defined in the KotlinActivity instead.
In KotlinActivity:
The above code in your question : )
Edit
I've created a demo project on Github showing the exact problem. Git Project.
I've written an expandable recyclerView in Kotlin Every row has a play button which uses TextToSpeech. The text of the play button should change to stop whilst its playing, then change back to play when it finishes.
When I call notifyItemChanged within onStart and onDone of setOnUtteranceProgressListener, onBindViewHolder is not called and the rows in the recyclerView will no longer expand and collapse correctly.
t1 = TextToSpeech(context, TextToSpeech.OnInitListener { status ->
if (status != TextToSpeech.ERROR) {
t1?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {
recyclerView.adapter.notifyItemChanged(position)
}
override fun onStop(utteranceId: String?, interrupted: Boolean) {
super.onStop(utteranceId, interrupted)
onDone(utteranceId)
}
override fun onDone(utteranceId: String?) {
val temp = position
position = -1
recyclerView.adapter.notifyItemChanged(temp)
}
override fun onError(utteranceId: String?) {}
// override fun onError(utteranceId: String?, errorCode: Int) {
// super.onError(utteranceId, errorCode)
// }
})
}
})
onBindViewHolder:
override fun onBindViewHolder(holder: RabbanaDuasViewHolder, position: Int){
if (Values.playingPos == position) {
holder.cmdPlay.text = context.getString(R.string.stop_icon)
}
else {
holder.cmdPlay.text = context.getString(R.string.play_icon)
}
}
How can I call notifyItemChanged(position) from within setOnUtteranceProgressListener or what callback can I use so that notifyItemChanged only executes when it's safe to execute?
I tried to replicate your issue and I came to know that it is not working because methods of UtteranceProgressListener is not called on main thread and that's why onBindViewHolder method of the adapter is not called.
This worked for me and should work for you too:
Use runOnUiThread{} method to perform actions on RecyclerView like this:
t1.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onError(utteranceId: String?) {
}
override fun onStart(utteranceId: String?) {
runOnUiThread {
recyclerView.adapter?.notifyItemChanged(position)
}
}
override fun onStop(utteranceId: String?, interrupted: Boolean) {
super.onStop(utteranceId, interrupted)
onDone(utteranceId)
}
override fun onDone(utteranceId: String?) {
val temp = position
position = -1
runOnUiThread {
recyclerView.adapter?.notifyItemChanged(temp)
}
}
}
I solved the problem using runOnUiThread thanks to Birju Vachhani.
For a full working demo of not just the problem I had, but how to correctly expand and collapse rows in a RecyclerView (no onClick events in onBindViewHolder) see my Gitlab Demo Project.
I am working on an android app where user get points for using the app which can be used to unlock in-app features.
I have a function called rewardPoints() which generates random integer and I want it to get called randomly while the user is using the app. The points then gets added up in database.
fun rewardPoints() {
var points = Random().nextInt((5-1) + 1)
}
How do I call the function rewardPoints() randomly while the user is using/interacting with the app?
I'd use a Handler to post a Runnable that re-posts itself. Like so,
val handler = Handler()
handler.post({
rewardPoints()
handler.postDelayed(this, DELAY_TIME_MS)
})
You could kick this off in your Activity's onResume and stop it onPause to make sure it's only running when the app is active.
You could add an observer on your activities, check whether you have active activities and when that's the case start a periodic task to award points.
Sample:
class MyApp : Application(), Application.ActivityLifecycleCallbacks {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(this)
}
var count: Int by Delegates.observable(0) { _, old, newValue ->
when (newValue) {
0 -> onBackground()
1 -> if (old == 0) onForeground()
}
}
override fun onActivityResumed(activity: Activity?) {
count++
}
override fun onActivityPaused(activity: Activity?) {
count--
}
fun onForeground() {
Log.d("TAG", "start.")
events.start()
}
fun onBackground() {
Log.d("TAG", "stop.")
events.cancel()
}
val events = object: CountDownTimer(Long.MAX_VALUE, 1000) {
// is called once per second as long as your app is in foreground
override fun onTick(millisUntilFinished: Long) {
if (ThreadLocalRandom.current().nextInt(100) < 5) {
Toast.makeText(this#MyApp, "You earned a point.", Toast.LENGTH_SHORT).show()
}
}
override fun onFinish() { /* will never happen */}
}
/* not needed */
override fun onActivityStarted(activity: Activity?) {}
override fun onActivityDestroyed(activity: Activity?) {}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
override fun onActivityStopped(activity: Activity?) {}
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {}
}
If you use architecture components Lifecycle, implementing above is even simpler with https://developer.android.com/reference/android/arch/lifecycle/ProcessLifecycleOwner and listening to the desired Lifecycle.Event
I need to do some input validation for my TextInputEditText that is wrapped with TextInputLayout.
I'd like errors to appear below the line if the input is done in wrong format.
All the logic is done in the viewmodel instead of the view(fragment or activity). But I can't seem to access the view through viewmodel, for instance:
textinputlayout.setError("error") doesn't work in the viewmodel
and layout.findViewbyId(layoutId) doesn't work in the viewmodel either.
Any idea?
used below code to set error in TextInputLayout..
class SpinerActivity :AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.spiner)
setEdittext()
}
private fun setEdittext() {
var textError:TextInputLayout=findViewById(R.id.amEt1)
textError.error="Please Enter Name"
}
}
I am trying to save the text contents of a TextView when my app closes, so I am trying to save that info in the onDestroy method, and then set them in the onCreate method.
I have written the following 2 functions to get rid of the boilerplate of getting and putting a value in the shared prefs:
fun MainActivity.putStringInPrefs(prefsFile: String, key: String, value: Any) =
getSharedPreferences(prefsFile, Context.MODE_PRIVATE)
.edit()
.putString(key, value.toString())
.apply()
fun MainActivity.getStringFromPrefs(prefsFile: String, key: String, default: Any = "") : String =
getSharedPreferences(prefsFile, Context.MODE_PRIVATE).getString(key, default.toString())
When the app closes this is what gets called:
override fun onDestroy() {
super.onDestroy()
log("OnDestroy!")
putStringInPrefs(mainPrefsFile, "lastSelectedItemDescription", textViewItemDetailsTextView.text) }
log is just a wrapper around Log.d("", "string")
And in the onCreate this gets called:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) ; log("OnCreate!")
setContentView(R.layout.activity_main)
// Set up the item details text view
textViewItemDetailsTextView.text = getStringFromPrefs(mainPrefsFile, "lastSelectedItemDescription", detailsText)
Problem is that whatever I do the preferences are not saved and the default value is always returned in onCreate. I have tried both apply and commit, with and without clear. No results. What am I doing wrong.
super.onDestroy() should be the last line in your overridden method onDestroy(). You might experience unexpected behaviour on whatever code you execute after "destructive" code has been executed
Save Activity state in onDestroy() is not good idea, because it is possible for the system to kill the process hosting your activity without calling the activity's final onDestroy() callback. (see docs).
To save state you can use onSaveInstanceState / onRestoreInstanceState instead. See: Saving and restoring activity state
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putString(YOUR_TEXT, textView.text)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
if (savedInstanceState != null) {
textView.text = savedInstanceState.getString(YOUR_TEXT)
}
}
companion object {
val YOUR_TEXT = "your_text"
}