How to group the duplicated statement in .apply in Kotlin? - android

Here is my code. As you can see the body of apply is exactly the same. Is there a better than use extension function?
contentText?.let {
contentTextView?.apply {
visibility = View.VISIBLE
text = contentText
}
}
titleText?.let {
titleTextView?.apply {
visibility = View.VISIBLE
text = titleText
}
}
Here is my function
private fun setTextAndVisiblity(textView: TextView?, newText: String?): TextView? {
return textView?.apply {
visibility = View.VISIBLE
text = newText
}
}
This is my code when apply function
contentText?.let {
setVisibleText(contentTextView, it)
}
titleText?.let {
setVisibleText(titleTextView, it)
}

I would write the extension function like this:
fun TextView.setVisibleIfTextNotNull(text: CharSequence?) = text?.let {
visibility = View.VISIBLE
this.text = it
}
Usage:
contentTextView?.setVisibleIfTextNotNull(contentText)
titleTextView?.setVisibleIfTextNotNull(titleText)
You can either make it as an nested function or private extension function as you like. The name of the function may not be clear enough to clarify what the function does, you may think of a better one.

An extension function seems like the best choice. If you make the function return this you can use it without apply.
Your other choice would be to create an ordinary function and pass it into also using method references, e.g.
fun setVisibleText(view: View) { }
titleTextView.also(this::setVisibleText)

the cleanest for me is to declare an extension function as:
fun TextView.setVisibleWithText(text: String?){
text ?: return
visibility = View.VISIBLE
setText(text)
}
then calling it as:
myTextView?.setVisibleWithText(myText)
Anyway,remember that an extension functions it just an static util function.
This function below:
fun TextView.setVisibleWithText(text: String){
visibility = View.VISIBLE
setText(text)
}
Will become something like this in java:
class TextViewKt {
public static function setVisibleWithText(#NotNull TextView receiver, #NotNull String text){
receiver.visibility = View.VISIBLE
receiver.setText(text)
}
}
And after you can call it as:
theText?.let { theTextView?.setVisibleWithText(it) }
You can always declare a normal funtion as:
fun setVisibleWithText(textView: TextView, text: String){
textView.visibility = View.VISIBLE
textView.text = text
}
or if you want to make the check inside:
/***
* It makes the textview visible if text is not null (it will stay visible if it was visible before)
**/
fun setVisibleWithText(textView: TextView, text: String?){
text ?: return
textView.visibility = View.VISIBLE
textView.text = text
}

Related

StateFlow collect does not change the data correctly

I'm using MVI architecture, coroutine and flow,
when I receive the data from the API, the status changed from LOADING to SUCCESS, and when I collect the stateFlow variable it submit the recyclerView successfully while when I try to hide the loading view (progressBar, lottie) the view freeze for a moment and it does not disappear.
I tried two ways
I tried to use the stateFlow in XML like this: android:visibility="#{viewModel.writersFlow.status == Results.Status.LOADING ? View.VISIBLE : View.GONE}", and of course I put lifecycleOwner = viewLifecycleOwner in onViewCreated function and I passed the viewModel to the XML
change the the visibility programmatically like bellow:
Repository:
override suspend fun getWriters(): Flow<Results<BaseModel<WriterModel>?>> =
resultFlowData(
networkCall = {
remoteDataSource.getResult {
endpoints.getWriters()
}
}
)
ViewModel:
private val _writersFlow: MutableStateFlow<Results<BaseModel<WriterModel>?>> =
MutableStateFlow(start())
val writersFlow: StateFlow<Results<BaseModel<WriterModel>?>>
get() = _writersFlow.asStateFlow()
private fun fetchWriters() {
viewModelScope.launch(Dispatchers.IO) {
writerRepository.getWriters().collect {
"writers: $it".log()
_writersFlow.emit(it)
}
}
}
Fragment: here in fragment you will see the binding.loading.visibility = View.GONE in both cases (SUCCESS, ERROR)
private fun gettingWriters() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.writersFlow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
when (it.status) {
Results.Status.SUCCESS -> {
writersAdapter.submitList(it.data?.response)
binding.loading.visibility = View.GONE
}
Results.Status.ERROR -> {
"$HOME_FRAGMENT: ${it.status}, ${it.code}, ${it.message}".log()
if (!it.message.isNullOrEmpty()) binding.root.snack(it.message ?: "") {}
binding.loading.visibility = View.GONE
}
Results.Status.LOADING -> {
binding.loading.visibility = View.VISIBLE
}
else -> {}
}
}
}
}
In the first way the loading view does not even appear but in the second way it appears but never disappear.

What to return in this recursive method?

I'm creating a method to recursively search for a View inside an ArrayList. It will loop through this ArrayList and, if it contains an ArrayList, it will be searched for Views too, and so on, until finding a View to return. This is so I can make whatever View is inside there invisible.
fun searchForView(arrayList: ArrayList<*>): View {
arrayList.forEach { item ->
if (item is View) {
return item
} else if (item is ArrayList<*>) {
item.forEach {
searchForView(it as ArrayList<*>)
}
}
}
} // Error here, needs to return a View
So I will use it like this:
someArrayList.forEach {
searchForView(someArrayList).visibility = View.INVISIBLE
}
However it is giving me an error because there needs to be a return someView statement near the end of the method. Whenever I call it, the ArrayList being searched will always have a View. So what should I be returning here at the end, knowing that whatever View found will already be returned?
You can set inside function and don't return anything
fun searchForView(arrayList: ArrayList<*>){
arrayList.forEach { item ->
if (item is View) {
item.visibility = View.INVISIBLE // set here
} else if (item is ArrayList<*>) {
item.forEach {
searchForView(it as ArrayList<*>)
}
}
}
}
You should use searchForView(item) instead of item.forEach { searchForView(it as ArrayList<*>) } as #IR42 suggested since you don't know each item in arraylist is an arraylist or not.
Your function is not compileable because it's supposed to return a View, but you aren't returning a View in the else branch or if you reach the end of the input list without finding a View.
However, if all this function does is return a View, then it is not usable for your requirement to set all views' visibility. It would only return a single View.
Instead, you can pass a function argument for what to do to each view it finds. There's no need to return anything.
fun ArrayList<*>.forEachViewDeep(block: (View) -> Unit) {
for (item in this) when (item) {
is View -> block(item)
is ArrayList<*> -> item.forEachViewDeep(block)
}
}
And use it like:
someArrayList.forEachViewDeep {
it.visibility = View.INVISIBLE
}
If it's very deeply nested, you might want to rearrange this function to be tail-recursive like this:
tailrec fun List<*>.forEachViewDeep(block: (View) -> Unit) {
for (item in this) {
if (item is View)
block(item)
}
filterIsInstance<ArrayList<*>>().flatten().forEachViewDeep(block)
}
I was trying to do the same thing like you before
and this is what I've made
class VisibilitySwitcher(private val mutableViewSet: MutableSet<View?>, private val onCondition: Boolean = true){
fun betweenVisibleOrGone(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.GONE}
View.GONE -> {it.visibility = View.VISIBLE}
}
}
}
fun betweenVisibleOrInvisible(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.INVISIBLE}
View.INVISIBLE -> {it.visibility = View.VISIBLE}
}
}
}
fun betweenInVisibleOrGone(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.INVISIBLE -> {it.visibility = View.GONE}
View.GONE -> {it.visibility = View.INVISIBLE}
}
}
}
}
Usage Example
class LoginActivity : BaseActivity() {
#Inject
#ViewModelInjection
lateinit var viewModel: LoginVM
private lateinit var mutableViewSet: MutableSet<View?>
override fun layoutRes() = R.layout.activity_login
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
facebookBtn.setOnClickListener { handleClickEvent(it) }
googleBtn.setOnClickListener { handleClickEvent(it) }
}
private fun handleClickEvent(view: View) {
when (view) {
facebookBtn -> { viewModel.smartLoginManager.onFacebookLoginClick() }
googleBtn -> { viewModel.smartLoginManager.onGoogleLoginClick() }
}
mutableViewSet = mutableSetOf(facebookBtn, googleBtn, progressBar)
VisibilitySwitcher(mutableViewSet).betweenVisibleOrGone() // <----- Use without Condition
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
VisibilitySwitcher(mutableViewSet, resultCode != -1).betweenVisibleOrGone() //<-- Use with Conditions
viewModel.smartLoginManager.onActivityResultCallBack(requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
}
The point is whenever you click login from facebook or google button
It will set visibility for facebook and google to be gone and set progressbar(the default of progressbar is View.GONE) to be visible
At override fun onActivityResult()
if the resultcode is not -1 it means that it got some error or cancel
so it will switch back the progressbar to be gone and change facebook and google button to be visible again
If you want to fix your own code I would do this
fun searchForView(mutableViewSet: MutableSet<View?>){
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.INVISIBLE}
View.INVISIBLE -> {it.visibility = View.VISIBLE} //<-- you can delete this if you don't want
}
}
}
Or very short form
fun searchForView(mutableViewSet: MutableSet<View?>) = mutableViewSet.forEach { when (it?.visibility) {View.VISIBLE -> it.visibility = View.INVISIBLE } }
Usage
val mutableViewSet = mutableSetOf(your view1,2,3....)
searchForView(mutableViewSet)
if it has to use arrayList: ArrayList<*> Then
fun searchForView(arrayList: ArrayList<*>) = arrayList.forEach{ if (it is View) it.visibility = View.INVISIBLE

Changing a textView based on the value of a variable using coroutines in kotlin

I have a boolean variable isConnected. I want to change a textView on the basis of this variable.
For example
if (isConnected):
textView text = a
else
textView text = b
This code should be running throughout the program. I tried to implement this in Android Studio but the app was unable to load anything.
var isConnected = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setStatusBar()
}
private fun setStatusBar() {
CoroutineScope(Main).launch {
while(true){
checkConnection()
}
}
}
#SuppressLint("SetTextI18n")
private fun checkConnection() {
CoroutineScope(Main).launch {
if(!isConnected){
status.text = "Disconnected"
}
else{
status.text = "Connected"
}
}
}
As I change the value of isConnected, I want the app to change the text present in the status textview,
Can someone tell me why my code is not working?
It is not a good practice too use infinite loops, Use Mutable LiveData to easily achieve this thing. You have to create a MutableLiveData variable isConnected of type Boolean and observer it value for changes to modify the text accordingly.
Variable declaration:
private val isConnected:MutableLiveData<Boolean> = MutableLiveData(false)
Now in onCreate observe it for changes:
isConnected.observe(this,Observer {
newValue ->
if(!newValue){
status.text = "Disconnected"
}
else{
status.text = "Connected"
}
})
Now to set the value use the below syntax:
isConnected.postValue(true)

Why is the Kotlin Synthetic is Null unless I use an explicit scope

So I have a bit of code here that used to work 1 month ago.
profile_clickable.throttleClicks {
logger.logEvent(PageTags.MENU_PROFILE_NAV)
edit_picture_button.visibility = View.GONE
ProfileActivity.start(this#HomeMenuActivity, avatar.transition(), username.transition())
}
This code now fails with an NPE on edit_picture_button, avatar, and username which are all Kotlin synthetics.
When I add an explicit call to each of those items (see below) suddenly it works.
profile_clickable.throttleClicks {
logger.logEvent(PageTags.MENU_PROFILE_NAV)
this#HomeMenuActivity.edit_picture_button.visibility = View.GONE
ProfileActivity.start(this#HomeMenuActivity, this#HomeMenuActivity.avatar.transition(), this#HomeMenuActivity.username.transition())
}
throttleClicks is an extension method that does this:
fun View.throttleClicks(
windowDurationMs: Long = 800,
onClick: View.() -> Unit
) {
setOnClickListener(object : View.OnClickListener {
// Set lastClickTime to - windowDurationMs to ensure the first click won't be throttled.
var lastClickTime = -windowDurationMs
override fun onClick(v: View?) {
val time = SystemClock.elapsedRealtime()
if (time - lastClickTime >= windowDurationMs) {
lastClickTime = time
onClick()
}
}
})
}
Why do I suddenly have to use an explicit scope to avoid NPEs?
Because you use synthetics in functiun of type View.() -> Unit.
So this in function is view on whitch you apply this function (profile_clickable).
Kotlin synthetics works like
val View.profile_clickable: ImageView get() {
if (cache exists) {
return cache
}
return this.findViewById(R.id.profile_clickable)
}
profile_clickable hasn't any childs, so there will be exception.
You can use this code:
profile_clickable.throttleClicks {
logger.logEvent(PageTags.MENU_PROFILE_NAV)
this#HomeMenuActivity.run {
edit_picture_button.visibility = View.GONE
ProfileActivity.start(this, avatar.transition(), username.transition())
}
}

How to use selectableButtonBackground on Anko?

How do I use selectableButtonBackground attribute on a custom View that uses Anko's apply() method inside its constructor like the following structure?
class XPTO(context: Context) : CardView(context) {
init {
this.apply {
// I'd like to invoke selectableButtonBackground here
}
}
I've tried to do context.obtainStyledAttributes(arrayOf(R.attr.selectableItemBackground).toIntArray()).getDrawable(0) but with no success.
I just created an extension function to get the resource ids for attributes.
val Context.selectableItemBackgroundResource: Int get() {
return getResourceIdAttribute(R.attr.selectableItemBackground)
}
fun Context.getResourceIdAttribute(#AttrRes attribute: Int) : Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.resourceId
}
This way you can also add more attributes if needed. Example to put it in anko:
frameLayout {
textView {
text = "Test"
backgroundResource = selectableItemBackgroundResource
isClickable = true
}
}
Don't forget the isClickable, else you won't see anything when you're clicking the textView
Another way to achieve this with Anko:
val backgroundResource = attr(R.attr.selectableItemBackgroundBorderless).resourceId

Categories

Resources