Observer is called twice at the same time in Kotlin - android-studio

When the button is clicked, data is received from the API, after which the Observer is fired, however, even with removeObservers, it is called twice.
Without removeObservers it triggers more than 2 times.
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestApiAppTheme {
Surface() {
TextInfo()
}
}
}
}
#Preview
#Composable
fun TextInfo() {
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
var txt = remember {
mutableStateOf(0)
}
viewModel.serverInfoResponse.removeObservers(this)
viewModel.serverInfoResponse.observe(this) {
txt.value = it.players
Toast.makeText(this, "${it.players} -", Toast.LENGTH_SHORT).show()
}
Column() {
Text(
text = txt.value.toString(),
)
Button(onClick = {
viewModel.getServerInfo()
visible = true
}) {
Text("Update")
}
}
}}
ViewModel
var serverInfoResponse = MutableLiveData<ServerInfo>()
fun getServerInfo() {
viewModelScope.launch {
val apiService = ApiService.getInstance()
val info = apiService.getInfo()
serverInfoResponse.value = info
}
}

A composable function can be called multiple times, whenever the it observes changes removeObserver() and observe() should not be called directly but, rather in some effect.
The extension function observeAsState() does all work to subscribe and correctly unsubscribe for you.

Composable functions can get called many times although the compiler tries to reduce how often.
You should not have side effects in them such as adding and removing the observer. Since LiveData sends the last item out to new subscribers, every time this function composes you get the value again.
The correct way to consume live data is to convert it to a state. https://developer.android.com/reference/kotlin/androidx/compose/runtime/livedata/package-summary
But then you still have problems with showing the toast too often. For that you will need something like a LaunchEffect to make sure you only show a toast once. Might want to consider if you really need a toast or if a compose type UI would be better.

Related

Why my savedInstanceState is not working?

I'm new to android studio with kotlin.
I want to make multiple choice quiz app, and I use data class and object constant to supply problem. If users choose correct choice, private var mCurrentPosition(Int) get plus 1 and setQuestion() work to change the problem, choices, and correctChoice.
To prevent the progress from being reset after the app is closed, I thought it would be okay if the int of mCurrentPosition was stored, so I use onSaveIntanceState. But progress is initialized after the app is closed...
class QuizActivity : AppCompatActivity() {
private var mCurrentPosition: Int = 1
private var mQuestion300List: ArrayList<Question300>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(savedInstanceState != null) {
with(savedInstanceState) {
mCurrentPosition = getInt(STATE_SCORE)
}
} else {
mCurrentPosition = 1
}
setContentView(R.layout.activity_quiz)
val questionList = Constant.getQuestions()
Log.i("Question Size", "${questionList.size}")
mQuestion300List = Constant.getQuestions()
setQuestion()
tv_choice1.setOnClickListener {
if (tv_correctChoice.text.toString() == "1") {
mCurrentPosition ++
setQuestion()
} else {
tv_choice1.setBackgroundResource(R.drawable.shape_wrongchoice)
}
}
tv_choice2.setOnClickListener {
if (tv_correctChoice.text.toString() == "2") {
mCurrentPosition ++
setQuestion()
} else {
tv_choice2.setBackgroundResource(R.drawable.shape_wrongchoice)
}
}
tv_choice3.setOnClickListener {
if (tv_correctChoice.text.toString() == "3") {
mCurrentPosition ++
setQuestion()
} else {
tv_choice3.setBackgroundResource(R.drawable.shape_wrongchoice)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState?.run {
putInt(STATE_SCORE, mCurrentPosition)
}
super.onSaveInstanceState(outState)
}
companion object {
val STATE_SCORE = "score"
}
private fun setQuestion() {
val question300 = mQuestion300List!![mCurrentPosition-1]
tv_question.text = question300!!.question
tv_choice1.text = question300.choice1
tv_choice2.text = question300.choice2
tv_choice3.text = question300.choice3
tv_correctChoice.text = question300.correctChoice
tv_now.setText("${mCurrentPosition}")
tv_choice1.setBackgroundResource(R.drawable.shape_problem)
tv_choice2.setBackgroundResource(R.drawable.shape_problem)
tv_choice3.setBackgroundResource(R.drawable.shape_problem)
}
}
here is my app code. plz give me help :) thank you
savedInstanceState is really only meant for two things
surviving rotations, where the Activity gets destroyed and recreated
the system killing your app in the background - so when you return, the Activity needs to be recreated as it was, so the user doesn't see any difference between the app just being in the background, and the app being killed to save resources
When onCreate runs, if the Activity is being recreated from a previous state, you'll get a bundle passed in as savedInstanceState - this contains all the stuff you added in onSaveInstanceState before the app was stopped earlier. But if the user has closed the app (either by backing out with the back button, or swiping the app away in the task switcher etc.) then that's counted as a fresh start with no state to restore. And savedInstanceState will be null in onCreate (which is one way you can check if it's a fresh start or not).
So if you want to persist state even after the app is explicitly closed by the user, you'll need to use something else. Here's the docs on the subject - the typical way is to use SharedPreferences for small data, some kind of database like Room for larger state. DataStore is the new thing if you wanted to try that out

animation doesn't run properly

class DialFragment: DialogFragment() {
private lateinit var image001: ImageView
private val caller = object: FingerprintManager.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
listener.onDialClick(errString.toString(), "2")
dismiss()
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)
listener.onDialClick("yes","1")
var avp = image001.drawable as AnimatedVectorDrawable
avp.start()
dismiss()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
listener.onDialClick("no","3")
dismiss()
}
The animation called by avp.start() is not beeing shown, the dialog fragment directly dismisses after succesfull authentification. if i omit the dismiss()-Call below then it works properly. My question is why is this and how to fix it? And yes, i know FingerprintManager-API is deprecated, i am running this for test purposes.
dismiss() immediately ends the life of your DialogFragment, so it will have no more opportunity to show you any animations in the Fragment. You should dismiss after a delay (however long the animation is).
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)
listener.onDialClick("yes","1")
var avp = image001.drawable as AnimatedVectorDrawable
avp.start()
activity?.runOnUiThread(300L) { // put however long your animation takes
dismiss()
}
}

How to Pagination using retrofit to fetch next items from API on Scrolling in kotlin?

I want to make my recyclerview paginationscrolling using retrofit.
I already complete get Json data using retrofit. It means interface API is correct from API Document.
However, if I loaded more 20items. can not scroll more items in Client.
when I checked Server data. per one page can get maximum 20items.
For example, if I loaded 25items in my recyclerview.
page 0: 20, page 1: 5.
if I want scrolling all items, How can I make paginationscrolling for retrofit??
check some my code and help me..
Response
Interface API
#GET("/store/cart/mine")
fun getCart(#Header("Authorization") token: String?, #Query("page") page:Int): Call<CartResponse>
CartViewActivity
class CartViewActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefreshListener {
private val lastVisibleItemPosition: Int
get()= LinearLayoutManager.HORIZONTAL
private lateinit var scrollListener: RecyclerView.OnScrollListener
lateinit var mAdapter: CartItemRecyclerAdapter
#RequiresApi(Build.VERSION_CODES.N)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cart_view)
val page = 0
val token = SharedPreference.getTokenInfo(this)
Client.retrofitService.getCart(token,page).enqueue(object :Callback<CartResponse> {
override fun onResponse(call: Call<CartResponse>, response: Response<CartResponse>) {
if (response?.isSuccessful == true) {
swipeRefreshLo.setOnRefreshListener(this#CartViewActivity)
showdata(response.body()?.docs!!)
}else if(response?.isSuccessful==false) {
val er = Gson().fromJson(response.errorBody()?.charStream(), ErrorResponse::class.java)
if (er.code==60202) {
}
}
}
override fun onFailure(call: Call<CartResponse>, t: Throwable) {
}
})
}
private fun showdata(results: MutableList<cartDocs>) {
recycler_view.apply {
mAdapter=CartItemRecyclerAdapter(context,context as Activity, results)
recycler_view.adapter=mAdapter
recycler_view.layoutManager=LinearLayoutManager(context)
setRecyclerViewScrollListener()
}
}
private fun setRecyclerViewScrollListener() {
scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val totalItemCount = recyclerView.layoutManager!!.itemCount
if(totalItemCount == lastVisibleItemPosition + 1) {
Log.d("MyTAG", "Load new list")
recyclerView.removeOnScrollListener(scrollListener)
}
}
}
}
override fun onRefresh() {
swipeRefreshLo.isRefreshing = false
}
}
Firstly, you need to modify your adapter to be able to update existing data, instead of creating new adapter every time you fetch new data.
Adapter should be initialised before getting the data, to be able to call methods on it.
You should create a method inside the adapter, something like
fun updateData(results: MutableList<cartDocs>) {
dataSet.addAll(results)
notifyItemRangeInserted(start, newItemsSize)
}
Then, we you get response from server in onSuccess() you should call method above, and increment page, so the next time you load data, you get new items.
Data should be fetched first time when screen loads (page will be 0), and then when user scrolls to bottom of RV ().

Can't communication between DialogFragment and Activity using Observer pattern?

When you press the button to open a separate input window, there is a function to display the results toast.
class MainActivity : AppCompatActivity() {
val disposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val f = TestPopup()
usingRxJava(f)
//usingLiveData(f)
}
}
private fun usingRxJava(f: TestPopup) {
val subject = SingleSubject.create<String>()
f.show(supportFragmentManager, "TAG")
button.post {
f.dialog.setOnDismissListener {
val str = f.arguments?.getString(TestPopup.TEST_KEY) ?: ""
subject.onSuccess(str)
}
}
subject.subscribe({
Toast.makeText(this, "Accept : $it", Toast.LENGTH_SHORT).show()
}, {
}).addTo(disposable)
}
private fun usingLiveData(f: TestPopup) {
val liveData = MutableLiveData<String>()
f.show(supportFragmentManager, "TAG")
button.post {
f.dialog.setOnDismissListener {
val str = f.arguments?.getString(TestPopup.TEST_KEY) ?: ""
liveData.postValue(str)
}
}
liveData.observe(this, Observer {
Toast.makeText(this, "Accept : $it", Toast.LENGTH_SHORT).show()
})
}
override fun onDestroy() {
disposable.dispose()
super.onDestroy()
}
}
DialogFragment
class TestPopup : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_test, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button_test.setOnClickListener {
val arg = Bundle()
arg.putString(TEST_KEY, edit_test.text.toString())
arguments = arg
dismiss()
}
}
companion object {
const val TEST_KEY = "KEY"
}
}
(Sample Project Url : https://github.com/heukhyeon/DialogObserverPattern )
This sample code works in normal cases. However, the toast does not float after the following procedure.
Developer Option - Dont'keep activities enable
Open TestPopup, and enter your text. (Do not press OK button)
Press the home button to move the app to the background
The app is killed by the system.
Reactivate the app (Like clicking on an app in the apps list)
In this case, the text I entered remains on the screen, but nothing happens when I press the OK button.
Of course I know this happens because at the end of the activity the observe relationship between the activity and the Dialog is over.
Most of the code uses the implementation of the callback interface for that Dialog in the Activity to handle that case.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button_test.setOnClickListener {
val input = edit_test.text.toString()
(activity as MyListener).inputComplete(input)
dismiss()
}
}
class MainActivity : AppCompatActivity(), TestPopup.MyListener {
override fun inputComplete(input: String) {
Toast.makeText(this, "Accept : $input", Toast.LENGTH_SHORT).show()
}
}
But I think it's a way that doesn't match the Observer pattern, and I want to implement it using the Observer pattern as much as possible.
I'm thinking of getting a Fragment from the FragmentManager and subscribing again at onCreate, but I think there's a better way.
Can someone help me?
Your understanding of the problem is correct, except that the problem happens with any configuration changes, including screen rotation. You can reproduce issue without using the developer mode. Try this for example:
Open TestPopup, and enter your text. (Do not press OK button)
Rotate screen
See toast message not popping up.
Also note that your "observer pattern" implementation is not a proper observer pattern. Observer pattern has a subject and an observer. In your implementation, the activity is acting as both the subject and the observer. The dialog is not taking any part in this observer pattern, and using .setOnDismissListener is just another form of a listener pattern.
In order to implement observer pattern between the Fragment(the subject) and the Activity(the observer), the Activity needs to get the reference of the Fragment using the FragmentManager as you suggested. I suggest to use view model and establish observer pattern between view layer and view model layer instead.
RxJava example:
//MainViewModel.kt
class MainViewModel: ViewModel() {
val dialogText = PublishProcessor.create<String>()
fun postNewDialogText(text: String) {
dialogText.onNext(text)
}
}
// Activity
val disposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.dialogText.subscribe {
Toast.makeText(this, "Accept : $it", Toast.LENGTH_SHORT).show()
}.addTo(disposable)
button.setOnClickListener {
TestPopup().show(supportFragmentManager, "TAG")
// usingRxJava(f)
// usingLiveData(f)
}
}
override fun onDestroy() {
disposable.dispose()
super.onDestroy()
}
// Dialog Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Important!! use activity when getting the viewmodel.
val viewModel = ViewModelProviders.of(requireActivity()).get(MainViewModel::class.java)
button_test.setOnClickListener {
viewModel.postNewDialogText(edit_test.text.toString())
dismiss()
}
}

[Android / Kotlin]: When are views initialized in lifecycle?

I need to know the size of a Button (or any other view).
But none of the procedures in lifecycle (onCreate, onStart, OnResume) seem to know it, as the Button seems not yet to be initialized!
...
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private var servOffset: Int=0 // Value depends on Layout/Orientation and Device
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btPunkte.setOnClickListener { doPunkt(true) }
servOffset = btPR1.width/2 // 'btPR1' is a Button in 'Layout activity_main.*'
//ToDo: Doesn't work! = remains 0
}
override fun onResume() {
super.onResume()
// ToDo: unsolved! When do I get the size??
// onStart (or onResume) are invoked correctly, but don't know the value!
// ?? Button doesn't yet exist in Livecycle?!
servOffset = btPR1.width/2 // //ToDo: Still doesn't work!
anzeigeAktualisieren()
}
private fun anzeigeAktualisieren() {
// If ... move Button
btPR1.x += servOffset //ToDo: unsolved offset remains 0 ?!
}
private fun doPunkt(links:Boolean) {
anzeigeAktualisieren()
...
}
...
}
I did find "When are views drawn", and several other threads, but they didn't help me solve my problem.
You can try using viewTreeObserver.
val vto = button.viewTreeObserver
vto.addOnGlobalLayoutListener {
Log.e("Show me width", button.width.toString())
}
It is working, but it can and WILL be called several times!!!
Other option is to use Handler and postDelayed
Handler().postDelayed({
Log.e("Show me width2", button.width.toString())
}, 1000)
Yes, this is very bad practice, but it can save you in stupid situation :)
Good luck!

Resources