This activity registers a user in firebase auth, uploads info in Realtime database and uploads user picture in Storage.
This code (I don't know why) gets stuck when it registers the user. If you see, I've added Log statements to break this entire process. The log is like
STARTING PROCESS
BEFORE
INSIDE
No other statement. I think I am using coroutines correctly but I don't know why this program doesn't go further than this ^. Should I use callbacks? Am I using coroutines in a wrong way?A lso any other suggestion will be appreciated.
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
private lateinit var firebaseAuth : FirebaseAuth
private lateinit var firebaseStorage: FirebaseStorage
private lateinit var firebaseDatabase: FirebaseDatabase
val TAG ="SIGNUPATAG"
var selectedPhoto : Uri? = null
var IMAGE_RESPONE_CODE = 1;
var isOk = false;
val imageUrl : String = "."
var userUID = "."
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
firebaseAuth = FirebaseAuth.getInstance()
binding.signupTvSelectPhoto.setOnClickListener {
val intent = Intent();
intent.type = "image/*"
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent,"Select Pic"),IMAGE_RESPONE_CODE)
}
binding.signupBtnSignUp.setOnClickListener {
val email = binding.signupEtvEmail.text.toString()
if(email.isEmpty() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
binding.signupEtvEmail.error = "Invalid Email Address"
binding.signupEtvEmail.requestFocus()
return#setOnClickListener
}
if(binding.signupEtvName.text.length < 3) {
binding.signupEtvName.error= "Name should at least have 3 characters"
binding.signupEtvName.requestFocus()
return#setOnClickListener
}
val password = binding.signupEtvPassword.text.toString()
if(password.length < 4) {
binding.signupEtvPassword.error = "Password should at least have 4 characters."
binding.signupEtvPassword.requestFocus()
return#setOnClickListener
}
// All Okay
Log.d(TAG,"STARTING PROCESS")
binding.pbSignup.visibility = View.VISIBLE
createAccount(email,password,binding.signupEtvName.text.toString())
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == 1) {
if(data != null) {
selectedPhoto = data?.data
binding.signupImgvPhoto.setImageURI(selectedPhoto)
}
else {
val context = this
selectedPhoto = Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+ context.getResources().getResourcePackageName(R.drawable.profilepicnormall) + '/'
+ context.getResources().getResourceTypeName(R.drawable.profilepicnormall) + '/'
+ context.getResources().getResourceEntryName(R.drawable.profilepicnormall) )
}
}
}
private fun createAccount(email : String, password : String,name:String) {
val context = this
selectedPhoto = Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+ context.getResources().getResourcePackageName(R.drawable.profilepicnormall) + '/'
+ context.getResources().getResourceTypeName(R.drawable.profilepicnormall) + '/'
+ context.getResources().getResourceEntryName(R.drawable.profilepicnormall) )
lifecycleScope.async(Dispatchers.Main) {
async {
create(email,password)
}.await()
Log.d(TAG,"The isOk is $isOk")
if(isOk){
async {
Log.d(TAG,"in 1 async")
uploadImage()
}.await()
async {
Log.d(TAG,"in 2 async")
uploadDataToRealtimeDatabase(userUID,email,name,imageUrl)
}.await()
binding.pbSignup.visibility = View.GONE
val intent = Intent(applicationContext,MainActivity::class.java)
startActivity(intent)
finish()
}
binding.pbSignup.visibility = View.GONE
}
}
suspend fun create(email: String,password: String) {
Log.d(TAG,"BEFORE")
firebaseAuth.createUserWithEmailAndPassword(email,password).addOnCompleteListener(parent) {task ->
if(task.isSuccessful) {
Toast.makeText(this#SignUpActivity,"SignUp Successful.",Toast.LENGTH_SHORT).show()
isOk = true;
userUID = firebaseAuth.currentUser!!.uid
Log.d(TAG,"INSIDE")
return#addOnCompleteListener
}
else {
Log.d(TAG,"${task.exception} . ")
Toast.makeText(this#SignUpActivity,"SignUp Not Successful.",Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG,"AFTER")
}
suspend fun uploadDataToRealtimeDatabase(UID:String,userEmail: String,userName : String,url:String) {
Log.d(TAG,"in upload data")
val ref = FirebaseDatabase.getInstance("https://firechat-931d2-default-rtdb.asia-southeast1.firebasedatabase.app/")
.getReference("/users/$UID")
val userinfo = UserInfo(userEmail,UID,userName,url)
ref.setValue(userinfo).addOnSuccessListener {
Log.d(TAG,"UPLOADED USER INFORMATION")
}.addOnFailureListener{
Log.d(TAG,"${it.message} $it")
}
}
suspend fun uploadImage() : String {
Log.d(TAG,"in upload Image")
val profilePicName = "${firebaseAuth.uid}.profileImage"
var url = "."
val storage_reference = FirebaseStorage.getInstance("gs://firechat-931d2.appspot.com").getReference("/ProfileImages/$profilePicName")
storage_reference.putFile(selectedPhoto!!).continueWithTask { task ->
if (!task.isSuccessful) {
Log.d(TAG,"${task.exception}")
}
storage_reference.downloadUrl.addOnSuccessListener {
url = it.toString()
}.addOnFailureListener{
Log.d(TAG,"$it ${it.message}")
}
}
if(url.length < 2) {
Log.d(TAG,"Going with default url.")
url = "https://firebasestorage.googleapis.com/v0/b/firechat-931d2.appspot.com/o/ProfileImages%2FsqE6s03wgXQm7gl03xxQIM3JVQc2.profileImage?alt=media&token=640266a5-6611-4e09-b8ed-72ba8bdfdc1f"
}
Log.d(TAG,"returning the img url $url")
return url
}
}
I implemented only the signup part in a dummy app after fixing some issues with your code, here are my observations:
No other statement
My logs were as follows:
D/SIGNUPATAG: STARTING PROCESS
D/SIGNUPATAG: INSIDE
D/SIGNUPATAG: The isOk is true
D/SIGNUPATAG: in 1 async
D/SIGNUPATAG: in upload Image
D/SIGNUPATAG: in 2 async
D/SIGNUPATAG: in upload data
I think I am using coroutines correctly but I don't know why this program doesn't go further than this ^. Should I use callbacks? Am I using coroutines in a wrong way?
I don't think you're using coroutines as intended, Firebase calls are already asynchronous so you don't require to do it this way, you should use Firebase coroutines instead which have built-in support for this case. I am adding the changes that I made below. (Omitting the stuff that doesn't require any change)
build.gradle
//firebase coroutines dependency
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.2"
SignUpActivity.kt
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
.
.
override fun onCreate(savedInstanceState: Bundle?) {
.
.
}
private fun createAccount(email : String, password : String,name:String) {
lifecycleScope.launch(Dispatchers.Main) {
val createdUserJob = launch {
try {
Toast.makeText(this#SignUpActivity,"SignUp Successful.",Toast.LENGTH_SHORT).show()
isOk = true;
userUID = firebaseAuth.currentUser!!.uid
Log.d(TAG,"INSIDE")
firebaseAuth.createUserWithEmailAndPassword(email, password).await()
} catch (e: Exception) {
Toast.makeText(this#SignUpActivity,"SignUp Not Successful.",Toast.LENGTH_SHORT).show()
e.printStackTrace()
}
}
createdUserJob.join()
Log.d(TAG,"The isOk is $isOk")
if(isOk){
val uploadImageJob = launch {
Log.d(TAG,"in 1 async")
uploadImage()
}
uploadImageJob.join()
val uploadDataJob = launch {
Log.d(TAG,"in 2 async")
uploadDataToRealtimeDatabase(userUID,email,name,imageUrl)
}
uploadDataJob.join()
binding.pbSignup.visibility = View.GONE
val intent = Intent(applicationContext,MainActivity::class.java)
startActivity(intent)
finish()
}
binding.pbSignup.visibility = View.GONE
}
}
suspend fun uploadDataToRealtimeDatabase(UID:String,userEmail: String,userName : String,url:String) {
Log.d(TAG,"in upload data")
.
.
}
suspend fun uploadImage() : String {
Log.d(TAG,"in upload Image")
.
.
return "dummy"
}
}
Note: I have only tested the signup part, you need to test the image upload and upload data part on your own, I've removed the create(email: String,password: String) method as well.
Related
I'm using coroutines and Set the Internet Permissions in manifest Everything But cant display my data on app when INTERNET is OFF, I'm caching my API response and successfully stored it in the database but cant retrive it when the internet is off
My Code In Repository
class HomeActivityRepository(
private val photoDatabase: PhotoDatabase,
private val applicationContext: Context
) {
private var photoLiveData = MutableLiveData<List<Photos>>()
val errorMessage = MutableLiveData<String>()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getServicesAPICall(): MutableLiveData<List<Photos>> {
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = RetrofitClient.getInstance().create(ApiInterface::class.java)
val res = response.getServicesAPICall()
withContext(Dispatchers.Main) {
if (NetworkUtils.isInternetAvailable(applicationContext)) {
if (res.isSuccessful) {
photoDatabase.photoDao().insertMemes(res.body()!!)
photoLiveData.postValue(res.body())
} else {
onError("Error : ${res.message()}")
}
} else {
val photos = photoDatabase.photoDao().getPhotos()
photoLiveData.postValue(photos)
}
}
}
return photoLiveData
}
private fun onError(message: String) {
errorMessage.postValue(message)
}
}
What I want: I want to be able to load ads on one screen and show it on another screen. I found some code to do this but only with an interstitial ad, I tried to do this with a rewarded ad and it works but I don't know if it's fine/safe or if there is a better or easier way to do this.
My Code:
**AdMob kt file:**
var mRewardedAd: RewardedAd? = null
private val _isAdRewardGranted = MutableLiveData(RewardModel())
val isAdRewardGranted: MutableLiveData<RewardModel> get() = _isAdRewardGranted
fun showRewardedAd(context: Context, reward: String) {
val activity = context.findActivity()
if (mRewardedAd != null) {
mRewardedAd?.show(activity!!){
Timber.i("AdMob | User earned the reward.")
changeIsAdRewardGranted(true, reward)
}
} else {
Timber.i("AdMob | The rewarded ad wasn't ready yet.")
}
}
fun getIsAdRewardGranted(): MutableLiveData<RewardModel> {
return isAdRewardGranted
}
fun changeIsAdRewardGranted(isRewardGranted: Boolean, reward: String){
_isAdRewardGranted.value = RewardModel(isRewardGranted, reward)
}
data class RewardModel(
var isAdRewardGranted: Boolean = false,
val reward: String = ""
)
**ViewModel:**
private var _uiState by mutableStateOf(ShopState())
val uiState: ShopState get() = _uiState
val getIsAdRewardGranted = getIsAdRewardGranted()
fun onEvent(event: ShopEvents){
when (event){
is OnChangeIsRewardedAdShow -> {
_uiState = _uiState.copy(isRewardedAdShow = event.show)
}
is OnRewardGranted -> {
changeIsAdRewardGranted(false, event.reward)
Timber.i("Reward ${event.reward} granted")
}
}
}
**Screen:**
val isRewardGranted by shopViewModel.getIsAdRewardGranted.observeAsState()
LaunchedEffect(key1 = isRewardGranted){
if (isRewardGranted?.isAdRewardGranted == true){
shopViewModel.onEvent(ShopEvents.OnRewardGranted(isRewardGranted!!.reward))
}
}
LaunchedEffect(key1 = uiState.isRewardedAdShow){
if (uiState.isRewardedAdShow){
showRewardedAd(context, "clue")
loadRewardedAd(context)
shopViewModel.onEvent(OnChangeIsRewardedAdShow(false))
}
}
// on button click shopViewModel.onEvent(OnChangeIsRewardedAdShow(true))
// ad loaded from activity when app start
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.
The doWork() method returns the Result before completing the task, resulting in blocking the main thread.
Some of the answers to this question are: To make the thread wait until the task is completed.
I feel there could be a better way to handle it.
class CloudWorker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
val fireStore = FirebaseFirestore.getInstance()
#SuppressLint("LogNotTimber")
override fun doWork(): Result {
Log.i("thred_dowork"," :"+Thread.currentThread().name)
val appContext = applicationContext
val dataString = inputData.getString(KEY_NOTE_DATA)
val data = Gson().fromJson(dataString, NoteData::class.java)
makeStatusNotification("Uploading Notes", appContext)
Log.i("uri:"," ${data.uri}")
val resolver = appContext.contentResolver
appContext.grantUriPermission(appContext.packageName,
Uri.parse(data.uri),
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val f = File(data.uri, "profile.jpg")
val picture = BitmapFactory.decodeStream(FileInputStream(f))
// Create a storage reference from our app
val storage = Firebase.storage
// Create a storage reference from our app
val storageRef = storage.reference
val baos = ByteArrayOutputStream()
picture.compress(Bitmap.CompressFormat.JPEG, 100, baos)
val imageDataX: ByteArray = baos.toByteArray()
var pathX = "images/"+System.currentTimeMillis().toString()+".jpg"
val mountainImagesRef = storageRef.child(pathX)
val uploadTask = mountainImagesRef.putBytes(imageDataX)
uploadTask.addOnSuccessListener {
Log.i("sync_","Image uploaded"+ it.metadata!!.path+" : "+it.metadata!!.name)
data.uri= it.metadata!!.path
fireStore.collection("notes")
.add(data)
.addOnSuccessListener {
Log.i("sync_","Sync Succes")
if(isStopped)
Log.i("sync_","Worker Stopped")
else
saveUrl()
}.addOnFailureListener {
Log.i("sync_","Sync Succes")
}
}.addOnFailureListener{
Log.e("sync_","Image not uploaded"+it.toString())
}
Timber.i("sync_data: $dataString")
Log.i("sync_","Called before firestore"+dataString)
return Result.success()
}
private fun saveUrl() {
Log.i("sync_","sleeping for 10 sec. thread_ "+Thread.currentThread().name)
sleep(15000)
if(isStopped)
Log.i("sync_","Worker Stopped")
else
Log.i("sync_","Worker not stopped")
Log.i("sync_","Wake up after 10 sec")
fireStore.collection("employee")
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result!!) {
Log.d("sync_employ", document.id + " => " + document.data)
}
} else {
Log.w("sync_", "Error getting documents.", task.exception)
}
}
}
}
Here I am doing firebase operation.
uploading image to firebase file storage
After successful upload, uploading content to the database.
On successful upload, retrieving some data from the database.
Your suggestions are most welcome here.
My goal is to capture or get a response from my viewModel into my signUp-Fragment when I click the signUp-Button and navigate to verification-Fragment if response.status is true.
When I click on my signUp button in my signUpFragment, a POST request retrofit call is made and a response is received like this :
UserResponse(message=Sign up successful. A verfication code has been sent to your email address, payload=UserPayload(country=Nigeria, createdAt=2020-04-10T10:55:06.220Z, email=osehiproductengineer#gmail.com, id=5e90508a455f70002f19b42e, isVerified=false, name=osehiase ehilen, phone=07083372454, updatedAt=2020-04-10T10:55:06.220Z, v=0), status=200, token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1ZTkwNTA4YTQ1NWY3MDAwMmYxOWI0MmUiLCJpYXQiOjE1ODY1MTYxMDYsImV4cCI6MTU4NjYwMjUwNn0.H_JhBQY-3PQ6Kqk7SS0cm8RP_1mzYlD987M66_LT0PU)
I saw this response using Log; the response does not get to my signUp-Fragment.
Here is my Repository code below:
class NetworkRepository(): BaseRepository() {
val userApi = UserAPI()
val authAPI = AuthAPI()
val treeAPI = TreeAPI()
val paymentAPI = PaymentAPI()
val loginAPI = LoginAPI()
val TAG = "NETWORK REPOSITORY"
private val _networkState = MutableLiveData<NetworkState>()
val networkState: LiveData<NetworkState>
get() = _networkState
//User
suspend fun createUser(userBody: UserBody): UserResponse {
var status = UserResponse()
// Log.d("SIGNUP_RESPONSE2", "inside status:$status")
withContext(Dispatchers.IO){
try {
status = userApi.addUserAsync(userBody).await()
// Log.d("signup_Response3", "after the call:$status")
}catch (t: Throwable){
Log.e(TAG, t.message.toString())
}
}
Log.d("SIGNUP_RESPONSE", "here is the $status")
return status
}
}
Here is my viewModel code:
class UserViewModel : ViewModel(){
private val repository = NetworkRepository()
private val job = Job()
private val scope = CoroutineScope(job + Dispatchers.Main)
fun createUser(userBody: UserBody):UserResponse {
var userPayload: UserResponse = UserResponse()
// Log.d("USERVIEWMODEL_TOP", "the first response:$userPayload")
scope.launch {
// userPayload = repository.createUser(userBody)
userPayload = repository.createUser(userBody)
// Log.d("USERVIEWMODELCHCK", "speak now:$userPayload")
}
// Log.d("USERVIEWMODEL_RESPONSE", "check this userViewModelRes:$userPayload")
return userPayload
}
}
Here is my SignUp-Fragment Code:
class SignUpFragment : Fragment() {
private lateinit var viewModel: UserViewModel
private lateinit var userBody: UserBody
var captureStatus:UserResponse = UserResponse()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
return inflater.inflate(R.layout.fragment_sign_up, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
signup_submit_btn.setOnClickListener {
val response = sendUser()
// Log.d("SIGNUP_FRAGMENTRES", "where is this response:$response")
if (response.status == 200) {
Log.d("SIGNUP_FRAGMENT", "wat is here:${response}")
saveUserInfo(response)
findNavController().navigate(R.id.action_signUpFragment_to_verificationFragment)
} else {
Toast.makeText(
this.context,
"${response.status}, ${response.message}",
Toast.LENGTH_SHORT
).show()
}
}
signup_have_an_account.paintFlags = Paint.UNDERLINE_TEXT_FLAG
signup_have_an_account.setOnClickListener {
findNavController().navigate(R.id.action_signUpFragment_to_loginFragment)
}
signup_back_btn.setOnClickListener {
findNavController().popBackStack()
}
}
private fun sendUser(): UserResponse {
var userBody: UserBody? = null
//verification
when {
signup_email_input.editText!!.text.isEmpty() -> {
signup_email_input.editText!!.error = "Email cannot be empty"
}
signup_phone_input.editText!!.text.isEmpty() -> {
signup_phone_input.editText!!.error = "Phone cannot be empty"
}
signup_country_input.editText!!.text.isEmpty() -> {
signup_country_input.editText!!.error = "Country cannot be empty"
}
signup_password_input.editText!!.text.isEmpty() -> {
signup_password_input.editText!!.error = "Password cannot be empty"
}
signup_password_input.editText!!.text.length < 6 -> {
signup_password_input.editText!!.error = "Password cannot be less than 6 characters"
}
signup_name_input.editText!!.text.isEmpty() -> {
signup_name_input.editText!!.error = "Name cannot be empty"
}
else -> {
val email = signup_email_input.editText!!.text.toString()
val country = signup_country_input.editText!!.text.toString()
val name = signup_name_input.editText!!.text.toString()
val password = signup_password_input.editText!!.text.toString()
val phone = signup_phone_input.editText!!.text.toString()
userBody = UserBody(country, email, false, name, password, phone)
}
}
// Log.d("USER REG", userBody.toString())
return viewModel.createUser(userBody!!)
}
private fun saveUserInfo(userResponse: UserResponse) {
this.activity?.let { Preferences.setEmail(it, userResponse.payload!!.email) }
this.activity?.let { Preferences.saveAuthToken(it, userResponse.token!!) }
}
}
userPayload = repository.createUser(userBody)
This line in your ViewModel will execute in background thread and hence is asynchronus, In order to publish "userPayload" object to your fragment, you need to make use of LiveData like this
//Define a mutablelivedata property in your ViewModel
public var userPayloadLiveData = MutableLiveData<UserResponse>()
From the co-routine inside your ViewModel you need to post your response to the livedata as such
userPayload = repository.createUser(userBody)
userPayloadLiveData.postValue(userPayload)
From your fragment you need to observe the "userPayloadLiveData" for async changes in value.
viewModel.userPayloadLiveData.observe(this, Observer { userResponse ->
//this code will run after network call and now safe to do UI stuff.
//userResponse is your response object
})
To understand more about how LiveData or MutableLiveData works please refer to androidx docs MutableLiveData
It's bad approach to instantiate variable (like: status, userPayload) with some Dummy Object, and then changing it with response from other method, then returning it via return function. You should better instantiate it with null and return the response to calling function via callback. Then if you have null you immediately now, something goes wrong.
The above approach is source of your problem. Because the createUser function is returning DummyObject, not actual object from Retrofit. To fix this, you need to delete return function from createUser method. And then add callback or higher-order function as second parameter.
Here is example of higher-order function, which is used as callback, when user is created:
createUser in ViewModel
fun createUser(userBody: UserBody, onUserCreated: (UserResponse) -> Unit) {
//Log.d("USERVIEWMODEL_TOP", "the first response:$userPayload")
scope.launch {
//userPayload = repository.createUser(userBody)
val userPayload: UserResponse = repository.createUser(userBody)
onUserCreated(userPayload)
//Log.d("USERVIEWMODELCHCK", "speak now:$userPayload")
}
}
Why this way? Because scope.launch{...} is something like closed environment and you have to get somehow userPayload from inside curly brackets scope.launch{ ... }. In your solution, you are returning userPayload from outisde of scope.launch{} which knows nothing about what happens inside {}
Also modify createUser function in your repository:
createUser in repository:
suspend fun createUser(userBody: UserBody): UserResponse {
return withContext(Dispatchers.IO) {
try {
val status = userApi.addUserAsync(userBody).await()
//Will be used as returend value.
status
} catch (t: Throwable) {
//Will be used as returned value - in case of error.
UserResponse()
}
}
}
Why? The same reason as above. You were returning status from outside of withContext(Dispatchers.IO) {}. So due to we want to return something from inside withContext(Dispatchers.IO) {} - we need to add return statement just before withContext. In this case last line from try {} or last line from catch{}(in case of error) will be used as return value for return statement before withContext(Dispatchers.IO) {}
And now you should be able to receive response in your fragment, by calling createUser function this way:
fragment
viewModel.createUser(userBody) { response ->
if (response.status == 200) {
Log.d("SIGNUP_FRAGMENT", "wat is here:${response}")
saveUserInfo(response)
findNavController().navigate(R.id.action_signUpFragment_to_verificationFragment)
} else {
Toast.makeText(
this.context,
"${response.status}, ${response.message}",
Toast.LENGTH_SHORT
).show()
}
}