sum20156
sum20156

Reputation: 736

How to get activity context in ViewModel Android?

I am using MVVM. I want to implement Firebase Auth in app. But to implement it I need an activity context in my repo class. How can I get it from ViewModel or is there any simple method available?

Here is the firebase code I need to implement:

 PhoneAuthProvider.getInstance().verifyPhoneNumber("+91"+phone,        // Phone number to verify
            60,                 // Timeout duration
            TimeUnit.SECONDS,   // Unit of timeout
            (Activity) context,               // Activity (for callback binding)
            new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
                @Override
                public void onVerificationCompleted(@NonNull PhoneAuthCredential phoneAuthCredential) 
               {
                    signInWithPhoneAuthCredential((Activity)context,phoneAuthCredential);
                }

                @Override
                public void onVerificationFailed(@NonNull FirebaseException e) {
                    setLoginFailed(e);

                }

                @Override
                public void onCodeSent(@NonNull String s, @NonNull 
                PhoneAuthProvider.ForceResendingToken forceResendingToken) {
                    super.onCodeSent(s, forceResendingToken);
                    loginResponse.setOnProgress(false);
                    loginResponse.setStatus(true);
                    loginResponse.setCodeVerified(false);
                    loginResponseLiveData.setValue(loginResponse);
                    verificationId =s;

                }
            });

Upvotes: 3

Views: 1022

Answers (4)

Thanasis M
Thanasis M

Reputation: 1361

Here are some good practices that you should always keep in mind:

Activity or Application context shouldn't be passed to a ViewModel

  • Memory Leaks: It may result in memory leaks. This is because the Context is tied to the lifecycle of the Activity or Fragment, and if the ViewModel retains a reference to it, the Activity or Fragment cannot be garbage collected when it is no longer needed. This can result in an unintended retention of resources and prevent the Activity or Fragment from being properly destroyed.
  • Separation of Concerns: By keeping a reference to the Context, it violates the separation of concerns, making the ViewModel aware of implementation details of the UI, which it should not be.
  • Testability: Having a Context reference in a ViewModel makes it harder to unit test. The ViewModel should be testable in isolation, meaning it should not depend on platform-specific classes like Context.
  • Lifecycle Management: The ViewModel is lifecycle-aware, but the Context is tied to the lifecycle of the UI components (such as Activity or Fragment). The ViewModel should not manage or interact directly with the lifecycle of the UI components. Instead, interactions with the Context should happen in the UI layer.

If activity context is explicitly needed you shouldn't use application context and cast it to Activity

  • Inappropriate Use of applicationContext: The applicationContext is designed to be a global context that is tied to the application's lifecycle, not to the specific lifecycle of an Activity.
  • Casting Is Unsafe: Casting applicationContext to an Activity is inherently unsafe. Since applicationContext is a global context and does not have the specific lifecycle and UI-related capabilities of an Activity, the cast can lead to ClassCastException if the context is not actually an instance of Activity. Even if the cast succeeds, it can result in undefined behavior, as the applicationContext doesn't carry the activity's lifecycle state and UI components.
  • Lifecycle Mismatch: Even if the cast doesn't fail, using applicationContext as an Activity could lead to problems because it is not lifecycle-aware. An Activity context can hold references to UI components, whereas the applicationContext does not.

Solution: Since March 28th 2023 in Firebase Android BoM version 31.4.0 (release notes), PhoneAuthOptions.Builder now accepts a null Activity, but it will throw a FirebaseAuthMissingActivityForRecaptchaException if app verification falls back to reCAPTCHA. This can happen if Play Integrity is unavailable or if the app fails Play Integrity checks. This issue has been previously raised in this Github issue. Therefore, you may now do what the Firebase docs suggest without calling .setActivity(this), like this:

val options = PhoneAuthOptions.newBuilder(auth)
    .setPhoneNumber(phoneNumber) // Phone number to verify
    .setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
    .setCallbacks(callbacks) // OnVerificationStateChangedCallbacks
    .build()
PhoneAuthProvider.verifyPhoneNumber(options)

Just make sure that you handle the possible FirebaseAuthMissingActivityForRecaptchaException error case in the onVerificationFailed callback as shown in the docs:

callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {

override fun onVerificationCompleted(credential: PhoneAuthCredential) {
    // This callback will be invoked in two situations:
    // 1 - Instant verification. In some cases the phone number can be instantly
    //     verified without needing to send or enter a verification code.
    // 2 - Auto-retrieval. On some devices Google Play services can automatically
    //     detect the incoming verification SMS and perform verification without
    //     user action.
    Log.d(TAG, "onVerificationCompleted:$credential")
    signInWithPhoneAuthCredential(credential)
}

override fun onVerificationFailed(e: FirebaseException) {
    // This callback is invoked in an invalid request for verification is made,
    // for instance if the the phone number format is not valid.
    Log.w(TAG, "onVerificationFailed", e)

    if (e is FirebaseAuthInvalidCredentialsException) {
        // Invalid request
    } else if (e is FirebaseTooManyRequestsException) {
        // The SMS quota for the project has been exceeded
    } else if (e is FirebaseAuthMissingActivityForRecaptchaException) {
        // reCAPTCHA verification attempted with null Activity
    }

    // Show a message and update the UI
}

override fun onCodeSent(
    verificationId: String,
    token: PhoneAuthProvider.ForceResendingToken,
) {
    // The SMS verification code has been sent to the provided phone number, we
    // now need to ask the user to enter the code and then construct a credential
    // by combining the code with a verification ID.
    Log.d(TAG, "onCodeSent:$verificationId")

    // Save verification ID and resending token so we can use them later
    storedVerificationId = verificationId
    resendToken = token
}
}

Upvotes: 0

Vikas
Vikas

Reputation: 468

  • If you work not related to any UI then you can use AndroidViewModel instead of ViewModel

  • In AndroidViewModel there in one important default parameter ApplicationContext

  • Or if you want any specific activity References then use LiveData<YourActivity> and set class reference when you initialize ViewModel in class

Upvotes: 1

cewaphi
cewaphi

Reputation: 410

First of all, according to official view model guidelines:

Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity content

So in accordance with your MVVM architecture, don't pass your activity context to a ViewModel.

I guess in that method you want to implement you do not specifically need an activity context. You could also use the context of your application.

Follow these 3 steps of this answer to get access your Application context statically:

  1. create Application class which references the context on create
    public class MyApplication extends Application {
    
        private static Context context;
    
        public void onCreate() {
            super.onCreate();
            MyApplication.context = getApplicationContext();
        }
    
        public static Context getAppContext() {
            return MyApplication.context;
        }
    }
    
  2. Declare your application class in your Manifest
    <application android:name="com.xyz.MyApplication">
    
    </application>
    
  3. Access this context statically inside your repository class
    MyApplication.getAppContext()
    

There is also the possibility to access the application context via AndroidViewModel class. But I guess you usually do not want to initialize your repository from your ViewModel.

Upvotes: 1

Doug Stevenson
Doug Stevenson

Reputation: 317487

In general, ViewModel objects are not supposed to reference anything that has to do with the Android platform APIs, especially Activity objects. You would not want a ViewModel to retain (and leak) an Activity across orientation changes. Retaining Activity objects in a ViewModel is a huge anti-pattern that should be avoided.

Instead, you should use a different version of that Firebase API. Choose one of the alternatives that do not take an Activity, according to the API documentation. Once the API is complete, you can bubble a callback up to the hosting activity to start any other activities.

Upvotes: 1

Related Questions