I implemented a registerForActivityResult in fragment to launch another activity and to get data from the activity. The problem is that the registerForActivityResult sometimes works and sometimes it doesn't. I put a log to be printed and I found that when it works the log is not printed. That means it doesn't go into the function.
val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result:ActivityResult ->
Log.i("register", "out")
if(result.resultCode == Activity.RESULT_OK)
{
val data = result.data
val note = data?.getParcelableExtra<Note>("note")
Log.i("register", "in")
adapter.mNotes[notePos].title = note?.title.toString()
adapter.mNotes[notePos].notes = note?.notes.toString()
adapter.notifyItemChanged(notePos)
}
}
private fun initRecycleView()
{
adapter = NotesAdapter(notesList){note, pos ->
val intent = Intent(this.context, NotingActivity::class.java).apply {
putExtra("note",note)
notePos = pos
}
getContent.launch(intent)
}
rvItems.adapter = adapter
rvItems.layoutManager = GridLayoutManager(this.context, 2)
(rvItems.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
override fun onBackPressed() {
intent.putExtra("note", note)
setResult(Activity.RESULT_OK, intent)
finish()
super.onBackPressed()
}
So I am starting to build a chat app and now I am at the registration screen.
Every time I press the login button,the request is sent only 1 time,like it should do.
The problem starts when I get in return the error message(e.g "Your password is incorrect"),after I get the error,I am pressing the login button again with the same wrong password,and I get Log error that I made but its showing 3 times, at the same time and firebase tells me that I have made too many attempts....
This is what I have done:
ViewModel:
private val _authState by lazy { MutableLiveData<AuthState>(AuthState.Loading) }
val authState: LiveData<AuthState> = _authState
fun loginUser(emailAddress: String, password: String) {
if (!isEmailAddressValid(emailAddress)) {
_authState.value = AuthState.AuthError("Invalid email")
return
} else if (password.isEmpty()) {
_authState.value = AuthState.AuthError("Password field can't be empty")
return
} else if (emailAddress.isEmpty()) {
_authState.value = AuthState.AuthError("Email field can't be empty")
return
}
auth.signInWithEmailAndPassword(emailAddress, password).addOnCompleteListener { task ->
if (task.isSuccessful) {
_authState.value = AuthState.Success
} else {
task.exception?.let {
_authState.value = AuthState.AuthError(it.localizedMessage)
}
}
}
}
This is the Activity:
binding.loginButton.setOnClickListener {
val emailEditText = binding.emailAddressEditText.text.toString()
val passwordEditText = binding.passwordEditText.text.toString()
registerLoginViewModel.loginUser(emailEditText, passwordEditText)
registerLoginViewModel.authState.observe(this#LoginRegisterActivity, object : Observer<AuthState?> {
override fun onChanged(loginState: AuthState?) {
when (loginState) {
is AuthState.Success -> {
hideLoadingScreen()
Toast.makeText(this#LoginRegisterActivity,"Welcome Back!",Toast.LENGTH_SHORT).show()
Intent(this#LoginRegisterActivity, MainActivity::class.java)
finish()
}
is AuthState.AuthError -> {
hideLoadingScreen()
Log.e("Error:","Error Message: ${loginState.message}") // This line returns 3 times after the second attempt
Toast.makeText(this#LoginRegisterActivity,loginState.message,Toast.LENGTH_SHORT).show()
}
else -> {
showLoadingScreen()
}
}
}
})
}
Thank you !
LiveData.observe(...) doesn't need to be in any kind of listener. You can observe in onCreate() of Activity ahead of API call. As it is in your code now, you're adding one new observer every time your click listener is called.
Here's a small example:
class FruitsActivity : AppCompatActivity {
private val binding by lazy {
FruitsActivityBinding.inflate(layoutInflater)
}
private val fruitsViewModel by viewModels<FruitsViewModel>()
#Override
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// Observe from fruitsViewModel.fruits
fruitsViewModel.fruits.observe(this) { fruitList ->
// Use `fruitList` in your adapter
}
// Fetch fruits on tap of a button
binding.loadFruitsButton.setOnClickListener {
fruitsViewModel.fetchFruits()
}
}
}
class FruitsViewModel : ViewModel() {
private val _fruits = MutableLiveData<List<Fruit>>()
val fruits: LiveData<List<Fruit>> = _fruits
fun fetchFruits() {
viewModelScope.launch {
// `someRepository` can be anything that calls an API
// or queries a database to get the required data.
// Repository Pattern + Coroutines recommended
val fruitList = someRepository.fetchFruits()
// if needed, perform any filters or modifications to `fruitList` here
// set the result data on LiveData
_fruits.value = fruitList
}
}
}
So, this is what happens:
Activity launches.
Initializes binding and fruitsViewModel.
Adds an Observer on fruits from fruitsViewModel
Sets click listener on a button to load fruits
When you tap the button, fruitsViewModel fetches fruits and sets result data on LiveData (_fruits).
LiveData finds its observers and notifies them about new data.
Let me know if you have any questions or if there's something wrong. I wrote the code directly in this text-field, so there might be a dot, comma or colon misplaced or missing.
I am pulling some data from the firestore and after that, I want to filter it before adding it to the recyclerView. Is that possible?
The data that I have pulled is an ArrayList which has a field called 'order_status'. It may contain many different statuses, but I want to filter it out so that I will be left with only "Pending", "Order Received", "In Process", "Packed".
The following code is used to pull the data from the firestore
fun getOrderStatusList(fragment: OrdersByStatusFragment) {
mFireStore.collection("orders")
.whereIn(
"address.pinCode",
listOf("676767", "652365","679577")
)
.get()
.addOnSuccessListener { document ->
val list: ArrayList<OrderStatus> = ArrayList()
for (i in document.documents) {
val orderStatus = i.toObject(OrderStatus::class.java)!!
orderStatus.id = i.id
list.add(orderStatus)
}
fragment.successOrderStatusList(list)
}
.addOnFailureListener {
fragment.hideProgressDialog()
}
}
The following code is part of the fragment.
fun successOrderStatusList(orderStatusList: ArrayList<OrderStatus>) {
hideProgressDialog()
if (orderStatusList.size > 0) {
rv_order_by_status.visibility = View.VISIBLE
tv_no_orders_by_status_found.visibility = View.GONE
rv_order_by_status.layoutManager = LinearLayoutManager(activity)
rv_order_by_status.setHasFixedSize(true)
val orderStatusListAdapter =
OrderStatusListAdapter(requireActivity(), orderStatusList,this#OrdersByStatusFragment)
rv_order_by_status.adapter = orderStatusListAdapter
} else {
rv_order_by_status.visibility = View.GONE
tv_no_orders_by_status_found.visibility = View.VISIBLE
}
}
In your case, you should just check for status inside the loop.
F.e:
if(document.orderStatus == "Pending")
//addTolist
I am using registerForActivityResult for google sign in implementation in my development. Everything was working fine until I upgraded my fragment dependency to 1.3.0-beta01. The application current crash with the error
java.lang.IllegalStateException: LifecycleOwner SignupChoicesFragment{8e0e269} (193105b9-afe2-4941-a368-266dbc433258) id=0x7f090139} is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
I have used the function before oncreate using lazy loading but it wont work still.
class SignupChoicesFragment : DaggerFragment() {
#Inject
lateinit var viewModelProviderFactory: ViewModelFactory
val userViewModel: UserViewModel by lazy {
ViewModelProvider(this, viewModelProviderFactory).get(UserViewModel::class.java)
}
#Inject
lateinit var mGoogleSignInClient:GoogleSignInClient
val arg:SignupChoicesFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_signup_choices, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
google_sign_in_button.setOnClickListener {
val intent = mGoogleSignInClient.signInIntent
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult(), ActivityResultCallback {result->
if (result.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
task.addOnCompleteListener {
if (it.isSuccessful) {
val account: GoogleSignInAccount? =
it.getResult(ApiException::class.java)
val idToken = it.result?.idToken
val email = account?.email
val lastName = account?.familyName
val firstName = account?.givenName
val otherName = account?.displayName
val imageUrl = account?.photoUrl
val category = arg.category
val newUser = User()
newUser.firstName = firstName
newUser.lastName = lastName
newUser.otherName = otherName
newUser.category = category
newUser.email = email
newUser.imageUrl = imageUrl.toString()
userViewModel.currentUser = newUser
newUser.token = idToken
i(title, "idToken $idToken")
requireActivity().gdToast("Authentication successful", Gravity.BOTTOM)
val action = SignupChoicesFragmentDirections.actionSignupChoicesFragmentToEmailSignupFragment()
action.newUser = newUser
goto(action)
} else {
requireActivity().gdToast(
"Authentication Unsuccessful",
Gravity.BOTTOM
)
Log.i(title, "Task not successful")
}
}
} else {
Log.i(title, "OKCODE ${Activity.RESULT_OK} RESULTCODE ${result.resultCode}")
}
}).launch(intent)
}
}
For me, the issue was that I was calling registerForActivityResult within an onClickListener which was only invoked on clicking a button (the app at this point is in state RESUMED). Moving the call outside the button's onClickListener and into the Activity's onCreate method fixed it.
quote from documentation
registerForActivityResult() is safe to call before your fragment or activity is created, allowing it to be used directly when declaring member variables for the returned ActivityResultLauncher instances.
Note: While it is safe to call registerForActivityResult() before your fragment or activity is created, you cannot launch the ActivityResultLauncher until the fragment or activity's Lifecycle has reached CREATED.
so to solve your issue move your register call outside the onCreate() and put it in fragment scope, and on google_sign_in_button click-listener call launch function
Note: if you are using Kotlin-Android-Extention move your click-listener call to onViewCreated()
If you are using a Fragment, please make sure that you are NOT performing the registerForActivityResult on the activity. Fragments also have a registerForActivityResult and that's the one you should use.
you must remove val launcher = registerForActivityResult... out of the setOnClickListener, then save it in a variable, in your example is launcher and in the setOnClickListener execute the variable with .launch, in your example es launcher.
your code would look like this
google_sign_in_button.setOnClickListener {
val intent = mGoogleSignInClient.signInIntent
launcher.launch(intent)
}
private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult(), ActivityResultCallback {result->
if (result.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
task.addOnCompleteListener {
if (it.isSuccessful) {
val account: GoogleSignInAccount? =
it.getResult(ApiException::class.java)
val idToken = it.result?.idToken
val email = account?.email
val lastName = account?.familyName
val firstName = account?.givenName
val otherName = account?.displayName
val imageUrl = account?.photoUrl
val category = arg.category
val newUser = User()
newUser.firstName = firstName
newUser.lastName = lastName
newUser.otherName = otherName
newUser.category = category
newUser.email = email
newUser.imageUrl = imageUrl.toString()
userViewModel.currentUser = newUser
newUser.token = idToken
i(title, "idToken $idToken")
requireActivity().gdToast("Authentication successful", Gravity.BOTTOM)
val action = SignupChoicesFragmentDirections.actionSignupChoicesFragmentToEmailSignupFragment()
action.newUser = newUser
goto(action)
} else {
requireActivity().gdToast(
"Authentication Unsuccessful",
Gravity.BOTTOM
)
Log.i(title, "Task not successful")
}
}
} else {
Log.i(title, "OKCODE ${Activity.RESULT_OK} RESULTCODE ${result.resultCode}")
}
})
Source : https://medium.com/codex/android-runtime-permissions-using-registerforactivityresult-68c4eb3c0b61
registerForActivityResult() is safe to call before your fragment or activity is created, allowing it to be used directly when declaring member variables for the returned ActivityResultLauncher instances.
you should call registerForActivityResult before view created. member variables or onCreate()
If you are working with any third party library then it may happens that you can't see the "registerForActivityResult" in your code but it should be present in classes provided by that same library.
So in this case I will suggest to move out the lines which is related to that library from any listener to the onCreate method.
for example -
btnBackup.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final RoomBackup roomBackup = new RoomBackup(GoogleDriveActivity.this);
roomBackup.database(LocalDataBase.getInstance(getApplicationContext()));
roomBackup.enableLogDebug(true);
roomBackup.backupIsEncrypted(false);
roomBackup.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_INTERNAL);
roomBackup.onCompleteListener((success, message, exitCode) -> {
Log.d(TAG, "success: " + success + ", message: " + message + ", exitCode: " + exitCode);
if (success) roomBackup.restartApp(new Intent(getApplicationContext(), GoogleDriveActivity.class));
});
roomBackup.restore();
}
});
//// remove other code from listener and shift in onCreate
roomBackup = new RoomBackup(GoogleDriveActivity.this);
roomBackup.database(LocalDataBase.getInstance(getApplicationContext()));
roomBackup.enableLogDebug(true);
roomBackup.backupIsEncrypted(false);
roomBackup.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_INTERNAL);
roomBackup.maxFileCount(5);
roomBackup.onCompleteListener((success, message, exitCode) -> {
Log.d(TAG, "success: " + success + ", message: " + message + ", exitCode: " + exitCode);
if (success) roomBackup.restartApp(new Intent(getApplicationContext(), GoogleDriveActivity.class));
});
/// you can keep only required lines in listener
btnBackup.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
roomBackup.backup();
}
});
That's it!
Found the same issue and manage to get to work with some magic.
In my case, it was happening in an Activity, so I went about it as such:
//...other bits
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// doing the setup here
setupViews()
}
private fun setupViews() {
val buttonLauncher = navigator.gotoScreenForResult(this) { success ->
if (success) {
setResult(Activity.RESULT_OK)
finish()
}
}
binding.myButton.setOnClickListener {
buttonLauncher.launch(Unit)
}
Where the navigator.gotoScreenForResult would look like the following:
override fun gotoScreenForResult(context: AppCompatActivity, callback: (Boolean) -> Unit): ActivityResultLauncher<Unit> {
val contract = object : ActivityResultContract<Unit, Boolean>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return Intent(context, MyNextActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
return resultCode == Activity.RESULT_OK
}
}
return context.registerForActivityResult(contract) { callback(it) }
}
Just make sure the setupViews is done within the onCreate and not on the resume step.
Writing an Android app with Kotlin using Android Studio. I have several activities and each of them has similar buttons. I added global variable ACTID which references each Activity I have through the map.
Every button has android: onClick="onClick" in its XML file.
So, I tried to make a public function:
public fun allClick(view: View){
val context = ACTIVITY_DICT[ACTID]
val toast = Toast.makeText(context, ACTID.toString(), Toast.LENGTH_LONG)
toast.show()
when (view.id)
{
R.id.nextBtn -> {
val intentNext = Intent(context, ACTIVITY_DICT[ACTID+1]!!::class.java)
context?.startActivity(intentNext)
context?.finish()}
R.id.backBtn -> {
val intentBack = Intent(context, ACTIVITY_DICT[ACTID-1]!!::class.java)
context?.startActivity(intentBack)
context?.finish()}
}
}
However, I cannot set allCLick for onClick. How can I fix it? Would be grateful for any possible help.
You can make a base activity BaseActivity, implement allClick(view: View) method in it and inherit from it other activities:
class BaseActivity : AppCompatActivity() {
public fun allClick(view: View) {
val context = ACTIVITY_DICT[ACTID]
val toast = Toast.makeText(context, ACTID.toString(), Toast.LENGTH_LONG)
toast.show()
when (view.id) {
R.id.nextBtn -> {
val intentNext = Intent(context, ACTIVITY_DICT[ACTID+1]!!::class.java)
context?.startActivity(intentNext)
context?.finish()
}
R.id.backBtn -> {
val intentBack = Intent(context, ACTIVITY_DICT[ACTID-1]!!::class.java)
context?.startActivity(intentBack)
context?.finish()
}
}
}
}
Also add android: onClick="allClick" for every button in its XML file.