App Open Ads-
I want to show ads with the first launch "onStart() function" but,
The problem is that the Ads don't show in the first launch (onStart() function) it shows in the onResume() function after I exit the application and resume it.
Are there any Suggestions or Editing Guys?
MyApplication Class
class MyApplication : Application() {
private var appOpenManager: AppOpenManager? = null
override fun onCreate() {
super.onCreate()
MobileAds.initialize(this) {
Log.d("tag", "MobileAds init ")
}
appOpenManager = AppOpenManager(this)
ProcessLifecycleOwner.get().lifecycle.addObserver(appOpenManager!!.defaultLifecycleObserver)
}
}
AppOpenManager Class
class AppOpenManager(private val myApplication: MyApplication) :
Application.ActivityLifecycleCallbacks,
LifecycleObserver {
private var appOpenAd: AppOpenAd? = null
private var loadCallback: AppOpenAd.AppOpenAdLoadCallback? = null
private var currentActivity: Activity? = null
private var isShowingAd = false
private val adRequest: AdRequest
get() = AdRequest.Builder().build()
private val isAdAvailable: Boolean
get() = appOpenAd != null
companion object {
private const val AD_UNIT_ID = "ca-app-pub-3940256099942544/3419835294" //test id
}
init {
myApplication.registerActivityLifecycleCallbacks(this)
}
private fun fetchAd() {
if (isAdAvailable) {
return
} else {
Log.d("tag", "fetching... ")
loadCallback = object : AppOpenAd.AppOpenAdLoadCallback() {
override fun onAdFailedToLoad(p0: LoadAdError) {
super.onAdFailedToLoad(p0)
Log.d("tag", "onAppOpenAdFailedToLoad: ")
}
override fun onAdLoaded(ad: AppOpenAd) {
super.onAdLoaded(ad)
appOpenAd = ad
Log.d("tag", "isAdAvailable = true")
}
}
val request = adRequest
AppOpenAd.load(
myApplication,
AD_UNIT_ID,
request,
AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT,
loadCallback!!
)
}
}
fun showAdIfAvailable() {
Log.d("tag", "$isShowingAd - $isAdAvailable")
if (!isShowingAd && isAdAvailable) {
Log.d("tag", "will show ad ")
val fullScreenContentCallback: FullScreenContentCallback =
object : FullScreenContentCallback() {
override fun onAdDismissedFullScreenContent() {
appOpenAd = null
isShowingAd = false
fetchAd()
}
override fun onAdFailedToShowFullScreenContent(p0: AdError) {
}
override fun onAdShowedFullScreenContent() {
isShowingAd = true
}
}
appOpenAd!!.fullScreenContentCallback = fullScreenContentCallback
appOpenAd!!.show(currentActivity!!)
} else {
Log.d("tag", "can't show ad ")
fetchAd()
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
currentActivity = activity
}
override fun onActivityStarted(activity: Activity) {
currentActivity = activity
}
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
currentActivity = activity
}
override fun onActivityDestroyed(activity: Activity) {
currentActivity = null
}
var defaultLifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
Log.d("tag", "onStart()")
showAdIfAvailable()
}
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
Log.d("tag", "onCreate() ")
}
}
}
onStart calls showAdIfAvailable() which only shows one if isAdAvailable is true, but it's initialised as false - so it calls fetchAd() instead. All that does is store an ad (so isAdAvailable is true for the next time you hit onStart).
Maybe you want to call showAdIfAvailable() again from your success callback when you fetch one?
override fun onAdLoaded(ad: AppOpenAd) {
super.onAdLoaded(ad)
appOpenAd = ad
Log.d("tag", "isAdAvailable = true")
// you got an ad - try and display it
showAdIfAvailable()
}
Just be careful, because your ads are being fetched asynchronously, it's possible your Activity could be destroyed while that request is still ongoing. That means you'd set currentActivity to null, your fetch would succeed and run showAdIfAvailable, and that tries to use currentActivity.
You need to null-check that - none of this !! stuff
if (!isShowingAd && isAdAvailable) {
val fullScreenContentCallback = object : FullScreenContentCallback() {
...
}
// ensure neither of these are null -while we're using them-
// let and run basically create new variables that can't be changed by callbacks etc
currentActivity?.let { activity ->
appOpenAd?.run {
fullScreenContentCallback = fullScreenContentCallback
show(activity)
}
}
Not necessarily the most elegant way (it would be better to null-check first so you don't do anything you don't need to, like creating that callback object which might not be used) but I don't want to rewrite your code. You get the idea hopefully!
Related
I just upgraded from Admob 20.6.0 to 21.4.0 in an Android project. Up until the upgrade, Admob App Open ads were working as expected. In 21.4.0 I am getting an error in showAdIfAvailable()
Type mismatch. Required: Activity Found: Activity?
On this line:
appOpenAd?.show(currentActivity)
Code correct suggests the following, but the problem is this code never runs.
currentActivity?.let { appOpenAd?.show(it) }
Here's the full code for AppOpenManager:
class AppOpenManager(private val myApplication: MainActivity) : DefaultLifecycleObserver, Application.ActivityLifecycleCallbacks {
private var appOpenAd: AppOpenAd? = null
private var loadCallback: AppOpenAdLoadCallback? = null
private var currentActivity : Activity? = null
private var isShowingAd : Boolean = false
private var loadTime:Long = 0
/** Creates and returns ad request. */
private fun getAdRequest():AdRequest {
return AdRequest.Builder().build()
}
/** Utility method that checks if ad exists and can be shown. */
private fun isAdAvailable():Boolean {
return appOpenAd != null && wasLoadTimeLessThan4HoursAgo()
}
/** Constructor */
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.myApplication.registerActivityLifecycleCallbacks(this)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
/** Request an ad */
fun fetchAd() {
if (isAdAvailable()) {
// Have unused ad, no need to fetch another.
return
}
loadCallback = object : AppOpenAdLoadCallback() {
override fun onAdLoaded(ad: AppOpenAd) {
this#AppOpenManager.appOpenAd = ad
this#AppOpenManager.loadTime = (Date()).time
}
override fun onAdFailedToLoad(loadAdError: LoadAdError) {
println("onAppOpenAdFailedToLoad $loadAdError")
}
}
val request: AdRequest = getAdRequest()
AppOpenAd.load(myApplication, StaticData.appOpenID, request, AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT, loadCallback as AppOpenAdLoadCallback)
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
showAdIfAvailable()
println("onStart")
}
private fun showAdIfAvailable() {
// Only show ad if there is not already an app open ad currently showing and an ad is available.
if (!isShowingAd && isAdAvailable()){
println("Will show ad.")
val fullScreenContentCallback = object: FullScreenContentCallback() {
override fun onAdDismissedFullScreenContent() {
// Set the reference to null so isAdAvailable() returns false.
this#AppOpenManager.appOpenAd = null
isShowingAd = false
fetchAd()
}
override fun onAdFailedToShowFullScreenContent(adError: AdError) {}
override fun onAdShowedFullScreenContent() {
isShowingAd = true
}
}
appOpenAd?.fullScreenContentCallback = fullScreenContentCallback
appOpenAd?.show(currentActivity) // Type mismatch. Required: Activity Found: Activity?
}
else
{
println("Can not show ad... FETCH ONE INSTEAD!")
fetchAd()
}
}
/** Utility method to check if ad was loaded more than n hours ago. */
private fun wasLoadTimeLessThan4HoursAgo():Boolean {
val dateDifference = (Date()).time - this.loadTime
val numMilliSecondsPerHour:Long = 3600000
return (dateDifference < (numMilliSecondsPerHour * 4))
}
override fun onActivityPaused(activity: Activity) {
println("⚠️ onActivityPaused")
}
override fun onActivityStarted(activity: Activity) {
println("⚠️ onActivityStarted")
currentActivity = activity
}
override fun onActivityDestroyed(activity: Activity) {
println("⚠️ onActivityDestroyed")
currentActivity = null
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
println("⚠️ onActivitySaveInstanceState")
}
override fun onActivityStopped(activity: Activity) {
println("⚠️ onActivityStopped")
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
println("⚠️ onActivityCreated")
}
override fun onActivityResumed(activity: Activity) {
println("⚠️ onActivityResumed")
currentActivity = activity
}
}
And in MainActivity:
private var appOpenManager: AppOpenManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
appOpenManager = AppOpenManager(this)
}
How do I get Android Admob App Open Ads to show in 21.4.0
Type mismatch. Required: Activity Found: Activity?
this error came because they made a major change
Added #NonNull annotations in every method that previously did not explicitly define nullability. -- in 21.0.0
but the problem is this code never runs currentActivity?.let { appOpenAd?.show(it) }
That is a correct way to solve the above error. I'll assume here that currentActivity is null or addOpenAd is null so the show(it) function call doesn't happen. Without logcat it's hard to see the root cause is, but here are a few things to check. I think one of these will be the solution:
Ensure that onAdFailedToLoad doesn't get called by looking for the log line. This will make sure that appOpenAd is assigned.
Make sure you're testing on Q+, because only on those versions will your code work. On older versions currentActivity will never get assigned. Because the lifecycle callbacks from registerActivityLifecycleCallbacks will not be registered.
It could be a timing issue too, you might need an additional code path to prepare for this case:
App starts and ad starts loading in the background
(appOpenAd = null, currentActivity = null)
Activity is started and shown
(appOpenAd = null, currentActivity = !null)
showAdIfAvailable is called, but there's no ad yet
BUG: it'll start loading another one!
Ad finishes loading
(appOpenAd = !null, currentActivity = !null)
BUG: nothing else is called from onAdLoaded
I have implemented a custom Application class in my app which handles updating the app theme before the app start up.
I also registered a network callback to set a variable each time there is a connection change.
My application class is as such:
Application.kt
package com.th3pl4gu3.mes.ui
.....
class MesApplication : Application() {
companion object {
#Volatile
private var INSTANCE: MesApplication? = null
fun getInstance() =
INSTANCE ?: synchronized(this) {
INSTANCE
?: MesApplication().also { INSTANCE = it }
}
}
override fun onCreate() {
super.onCreate()
// Assigns 'this' to the singleton object
INSTANCE = this
// Updates the application's theme
updateAppTheme()
// Start a network callback to monitor internet connection
startNetworkCallback()
}
private fun startNetworkCallback(){
try{
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val builder = NetworkRequest.Builder()
cm.registerNetworkCallback(builder.build(), object: ConnectivityManager.NetworkCallback(){
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.v("INTERNET_TEST", "AC: Network Available")
Global.isNetworkConnected = true
}
override fun onLost(network: Network) {
super.onLost(network)
Log.v("INTERNET_TEST", "AC: Network Lost")
Global.isNetworkConnected = false
}
})
Global.isNetworkConnected = false
}catch (e: Exception){
Global.isNetworkConnected = false
}
}
}
However, from the docs, they recommend to unregister this callback but the Application class lifecycle doesn't have any onPause or onDestroy function.
Is there any proper way to unregister this callback to not cause any memory leaks?
Also feel free to suggest any alternatives in case I am coding this wrong
In this case , you can use ActivityLifecycleCallbacks, to detect are any Activity of your is in Foreground?
ActivityLiveCycleListener
class ActivityLiveCycleListener(private val appStateListener: AppStateListener) : Application.ActivityLifecycleCallbacks {
companion object {
var foregroundActivities = 0
}
override fun onActivityPaused(p0: Activity) {
}
override fun onActivityStarted(p0: Activity) {
if(foregroundActivities == 0){
appStateListener.onAppForeGround()
}
foregroundActivities++
}
override fun onActivityDestroyed(p0: Activity) {
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
}
override fun onActivityStopped(p0: Activity) {
foregroundActivities--
if(foregroundActivities == 0){
appStateListener.onAppBackground()
}
}
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
}
override fun onActivityResumed(p0: Activity) {
}
}
And your interface can have two methods to indicate background/foreground state
interface AppStateListener{
fun onAppForeGround()
fun onAppBackground()
}
Now in Application onCreate(), register to ActivityLifeCycleListener
override fun onCreate(){
registerActivityLifecycleCallbacks(ActivityLiveCycleListener(object : AppStateListener{
override fun onAppForeGround() {
//start network listener
}
override fun onAppBackground() {
//remove network listener
}
}))
}
my data is fetched only when it is created...im using viewmodel...when press back button it doesnt update the previous data..onresume is not working in this...
i refered this but none of those helped--> Reacting to activity lifecycle in ViewModel
i need help
thanks in advance
activity:--
class MyAccount : BaseClassActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
var mActionBarToolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable);
setSupportActionBar(mActionBarToolbar);
setEnabledTitle()
val resetbutton=findViewById<Button>(R.id.resetpwd)
resetbutton.setOnClickListener {
val i=Intent(applicationContext,
ResetPasswordActivity::class.java)
startActivity(i)
}
val editbutton=findViewById<Button>(R.id.editdetail)
editbutton.setOnClickListener {
val i=Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
hello()
}
override fun onResume() {
super.onResume()
hello()
}
fun hello(){
val first_name = findViewById<TextView>(R.id.firstname)
val last_name = findViewById<TextView>(R.id.lastname)
val emailuser = findViewById<TextView>(R.id.emailuser)
val phone_no = findViewById<TextView>(R.id.phone_no)
val birthday = findViewById<TextView>(R.id.birthday)
val image=findViewById<ImageView>(R.id.imageprofile)
val model = ViewModelProvider(this)[MyAccountViewModel::class.java]
model.viewmodel?.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext).load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}}
viewmodel:--
class MyAccountViewModel(context: Application) :AndroidViewModel(context),LifecycleObserver{
private var MyAccountViewModels: MutableLiveData<My_account_base_response>? = null
val viewmodel: MutableLiveData<My_account_base_response>?
get() {
if (MyAccountViewModels == null) {
MyAccountViewModels = MutableLiveData<My_account_base_response>()
loadviewmodel()
}
return MyAccountViewModels
}
private fun loadviewmodel(){
val token :String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
MyAccountViewModels!!.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}}
There are bunch of things wrong here, so let me provide you refactored code and explanation as much as I would be able to..
Activity:
class MyAccount : BaseClassActivity() {
private val mActionBarToolbar by lazy { findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable) }
private val resetbutton by lazy { findViewById<Button>(R.id.resetpwd) }
private val editbutton by lazy { findViewById<Button>(R.id.editdetail) }
private val first_name by lazy { findViewById<TextView>(R.id.firstname) }
private val last_name by lazy { findViewById<TextView>(R.id.lastname) }
private val emailuser by lazy { findViewById<TextView>(R.id.emailuser) }
private val phone_no by lazy { findViewById<TextView>(R.id.phone_no) }
private val birthday by lazy { findViewById<TextView>(R.id.birthday) }
private val image by lazy { findViewById<ImageView>(R.id.imageprofile) }
lateinit var model: MyAccountViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
setSupportActionBar(mActionBarToolbar)
setEnabledTitle()
model = ViewModelProvider(this)[MyAccountViewModel::class.java]
resetbutton.setOnClickListener {
val i = Intent(applicationContext, ResetPasswordActivity::class.java)
startActivity(i)
}
editbutton.setOnClickListener {
val i = Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
model.accountResponseData.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext)
.load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onResume() {
super.onResume()
model.loadAccountData()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
Few notes on your activity class:
You don't need to findViewById everytime, just do it once during onCreate or do it lazily. (FYI consider using kotlin synthetics or view binding or data binding)
Initialize your viewModel during onCreate method only. (That's the best way to do it)
Also observer your LiveData from ViewModel once, it should be also from the onCreate as it's the entry point to the activity and apart from config changes this method called only once. So, it's safe to observe it over there rather than during onResume which will be called multiple times during activity lifecycle. (The main issue your code wasn't working, so as a fix you only call your API method from ViewModel during resume)
ViewModel:
class MyAccountViewModel(context: Application) : AndroidViewModel(context) {
private val _accountResponseData = MutableLiveData<My_account_base_response?>()
val accountResponseData: MutableLiveData<My_account_base_response?>
get() = _accountResponseData
init {
loadAccountData()
}
fun loadAccountData() {
val token: String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
_accountResponseData.value = null
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
_accountResponseData.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(
getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}
}
Don't make initial API call along with LiveData creation, it's okay to do in most of cases but if you're updating LiveData on response of that call then it's good to make it separately like during init block.
It's good practice not to allow Ui (Activity/Fragments) to modify LiveDatas of ViewModel directly. So, that's good sign you're following such pattern by having private MutableLiveData exposed as public LiveData, but do it correctly as suggested.
Side note: Your view model doesn't need to be LifecycleObserver. LifecycleObserver is used for some custom class/component which needs to be managed by their self by silently observing/depending on activity lifecycle independently. That's not the use case of ViewModel.
The only thing that I found why your code wasn't working correctly is because you were creating & observing ViewModel & LiveData over & over again as new objects from onResume method where you called hello() method.
Let me know if something don't make sense or missing.
I have one activity with unspecified orientation and there is one fragment attached to that activity that has different layouts for portrait and landscape mode and on that fragment, multiple API calls on a conditional basis, my problem is that when the screen rotates all data was lost and there is a lot of data on that fragment by which I don't want to save each data on saveInstance method. I tried android:configChanges="keyboardHidden|orientation|screenSize", but this didn't solve my problem. I want to handle this problem using viewModel. Please help, Thanks in advance.
Here is my code
Repository
class GetDataRepository {
val TAG = GetDataRepository::class.java.canonicalName
var job: CompletableJob = Job()
fun getData(
token: String?,
sslContext: SSLContext,
matchId: Int
): LiveData<ResponseModel> {
job = Job()
return object : LiveData<ResponseModel>() {
override fun onActive() {
super.onActive()
job.let { thejob ->
CoroutineScope(thejob).launch {
try {
val apiResponse = ApiService(sslContext).getData(
token
)
LogUtil.debugLog(TAG, "apiResponse ${apiResponse}")
withContext(Dispatchers.Main) {
value = apiResponse
}
} catch (e: Throwable) {
LogUtil.errorLog(TAG, "error: ${e.message}")
withContext(Dispatchers.Main) {
when (e) {
is HttpException -> {
value =
Gson().fromJson<ResponseModel>(
(e as HttpException).response()?.errorBody()
?.string(),
ResponseModel::class.java
)
}
else -> value = ResponseModel(error = e)
}
}
} finally {
thejob.complete()
}
}
}
}
}
}
fun cancelJob() {
job.cancel()
}
}
ViewMode:
class DataViewModel : ViewModel() {
val TAG = DataViewModel::class.java.canonicalName
var mListener: DataListener? = null
private val mGetDataRepository: GetDataRepository = GetDataRepository()
fun getData() {
LogUtil.debugLog(TAG, "getData")
if (mListener?.isInternetAvailable()!!) {
mListener?.onStartAPI()
val context = mListener?.getContext()
val token: String? = String.format(
context?.resources!!.getString(R.string.user_token),
PreferenceUtil.getUserData(context).token
)
val sslContext = mListener?.getSSlContext()
if (sslContext != null) {
val getData =
mGetDataRepository.getData(
token
)
LogUtil.debugLog(TAG, "getData ${getData}")
mListener?.onApiCall(getData)
} else {
LogUtil.debugLog(TAG, "getData Invalid certificate")
mListener?.onError("Invalid certificate")
}
} else {
LogUtil.debugLog(TAG, "getData No internet")
mListener?.onError("Please check your internet connectivity!!!")
}
LogUtil.debugLog(TAG, "Exit getData()")
}
}
Activity:
class DataActivity : AppCompatActivity() {
val TAG = DataActivity::class.java.canonicalName
lateinit var fragment: DataFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LogUtil.debugLog(TAG, "onCreate: Enter")
var binding: ActivityDataBinding =
DataBindingUtil.setContentView(this, R.layout.activity_data)
if (savedInstanceState == null) {
fragment = DataFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.container, fragment, DataFragment.TAG)
} else {
fragment = supportFragmentManager.findFragmentByTag(DataFragment.TAG) as DataFragment
}
LogUtil.debugLog(TAG, "onCreate: Exit")
}
}
Fragment:
class DataFragment : Fragment(), DataListener {
private var mBinding: FragmentDataBinding? = null
private lateinit var mViewModel: DataViewModel
companion object {
val TAG = DataFragment::class.java.canonicalName
fun newInstance(): DataFragment {
return DataFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_data, container, false)
mViewModel = ViewModelProvider(this).get(DataViewModel::class.java)
mViewModel.mListener = this
getData()
return mBinding?.root
}
private fun getData() {
LogUtil.debugLog(TAG, "Enter getMatchScore()")
mViewModel.getData()
LogUtil.debugLog(TAG, "Exit getMatchScore()")
}
override fun <T> onApiCall(response: LiveData<T>) {
response.observe(this, Observer {
it as DataResponseModel
//
})
}
}
The lifecycle of viewModel by default is longer than your activity (in your case, screen rotation).
ViewModel will not be destroyed as soon as activity destroyed for configuration change, you can see this link.
You seem to have made a mistake elsewhere in your activity/fragment, please put your activity/fragment code here.
In your fragment you call mViewModel.getData() in your onCreateView, and every time you rotate your activity, this method call and all store data reset and fetched again!, simply you can check data of ViewModel in your fragment and if it's empty call getData(), it also seems your ViewModel reference to your view(Fragment) (you pass a listener from your fragment to your ViewModel) and it is also an anti-pattern (This article is recommended)
I have been trying, without success, to do some UI tests on Android.
My app follows the MVVM architecture and uses Koin for DI.
I followed this tutorial to properly set up a UI test for a Fragment with Koin, MockK and Kakao.
I created the custom rule for injecting mocks, setup the ViewModel, and on the #Before call, run the expected answers and returns with MockK. The problem is that, even when the fragment's viewmodel's LiveData object is the same as the testing class's LiveData object, the Observer's onChange is never triggered on the Fragment.
I run the test with the debugger and it seems the LiveData functions and MockK's answers are properly called. The logs show that the value hold by the LiveData objects is the same. The lifecycle of the Fragment when the test is running is Lifecycle.RESUMED. So why is the Observer's onChange(T) not being triggered?
The custom rule:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
abstract class FragmentTestRule<F : Fragment> :
ActivityTestRule<FragmentActivity>(FragmentActivity::class.java, true, true) {
override fun afterActivityLaunched() {
super.afterActivityLaunched()
activity.runOnUiThread {
val fm = activity.supportFragmentManager
val transaction = fm.beginTransaction()
transaction.replace(
android.R.id.content,
createFragment()
).commit()
}
}
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
val app = InstrumentationRegistry.getInstrumentation()
.targetContext.applicationContext as VideoWorldTestApp
app.injectModules(getModules())
}
protected abstract fun createFragment(): F
protected abstract fun getModules(): List<Module>
fun launch() {
launchActivity(Intent())
}
}
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <F : Fragment> createRule(fragment: F, vararg module: Module): FragmentTestRule<F> =
object : FragmentTestRule<F>() {
override fun createFragment(): F = fragment
override fun getModules(): List<Module> = module.toList()
}
My test App:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
class VideoWorldTestApp: Application(){
companion object {
lateinit var instance: VideoWorldTestApp
}
override fun onCreate() {
super.onCreate()
instance = this
startKoin {
if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
androidContext(this#VideoWorldTestApp)
modules(emptyList())
}
Timber.plant(Timber.DebugTree())
}
internal fun injectModules(modules: List<Module>) {
loadKoinModules(modules)
}
}
The custom test runner:
class CustomTestRunner: AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, VideoWorldTestApp::class.java.name, context)
}
}
The test:
#RunWith(AndroidJUnit4ClassRunner::class)
class HomeFragmentTest {
private val twitchViewModel: TwitchViewModel = mockk(relaxed = true)
private val userData = MutableLiveData<UserDataResponse>()
private val fragment = HomeFragment()
#get:Rule
var fragmentRule = createRule(fragment, module {
single(override = true) {
twitchViewModel
}
})
#get:Rule
var countingTaskExecutorRule = CountingTaskExecutorRule()
#Before
fun setup() {
val userResponse: UserResponse = mockk()
every { userResponse.displayName } returns "Rubius"
every { userResponse.profileImageUrl } returns ""
every { userResponse.description } returns "Soy streamer"
every { userResponse.viewCount } returns 5000
every { twitchViewModel.userData } returns userData as LiveData<UserDataResponse>
every { twitchViewModel.getUserByInput(any()) }.answers {
userData.value = UserDataResponse(listOf(userResponse))
}
}
#Test //This one is passing
fun testInitialViewState() {
onScreen<HomeScreen> {
streamerNameTv.containsText("")
streamerCardContainer.isVisible()
nameInput.hasEmptyText()
progressBar.isGone()
}
}
#Test //This one is failing
fun whenWritingAName_AndPressingTheImeAction_AssertTextChanges() {
onScreen<HomeScreen> {
nameInput.typeText("Rubius")
//nameInput.pressImeAction()
searchBtn.click()
verify { twitchViewModel.getUserByInput(any()) } //This passes
countingTaskExecutorRule.drainTasks(5, TimeUnit.SECONDS)
streamerNameTv.hasText("Rubius") //Throws exception
streamerDescp.hasText("Soy streamer")
streamerCount.hasText("Views: ${5000.formatInt()}}")
}
}
}
The fragment being tested:
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override val bindingFunction: (view: View) -> FragmentHomeBinding
get() = FragmentHomeBinding::bind
val twitchViewModel: TwitchViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
twitchViewModel.getUserClips("")
binding.nameInput.setOnEditorActionListener { _, actionId, _ ->
if(actionId == EditorInfo.IME_ACTION_SEARCH) {
twitchViewModel.getUserByInput(binding.nameInput.text.toString())
hideKeyboard()
return#setOnEditorActionListener true
}
return#setOnEditorActionListener false
}
binding.searchBtn.setOnClickListener {
twitchViewModel.getUserByInput(binding.nameInput.text.toString() ?: "")
hideKeyboard()
}
twitchViewModel.userData.observe(viewLifecycleOwner, Observer { data ->
if (data != null && data.dataList.isNotEmpty()){
binding.streamerCard.setOnClickListener {
findNavController().navigate(R.id.action_homeFragment_to_clipsFragment)
}
val streamer = data.dataList[0]
Picasso.get()
.load(streamer.profileImageUrl)
.into(binding.profileIv)
binding.streamerLoginTv.text = streamer.displayName
binding.streamerDescpTv.text = streamer.description
binding.streamerViewCountTv.text = "Views: ${streamer.viewCount.formatInt()}"
}
else {
binding.streamerCard.setOnClickListener { }
}
})
twitchViewModel.errorMessage.observe(viewLifecycleOwner, Observer { msg ->
showSnackbar(msg)
})
twitchViewModel.progressVisibility.observe(viewLifecycleOwner, Observer { visibility ->
binding.progressBar.visibility = visibility
binding.cardContent.visibility =
if(visibility == View.VISIBLE)
View.GONE
else
View.VISIBLE
})
}
}
The ViewModel:
class TwitchViewModel(private val repository: TwitchRepository): BaseViewModel() {
private val _userData = MutableLiveData<UserDataResponse>()
val userData = _userData as LiveData<UserDataResponse>
private val _userClips = MutableLiveData<UserClipsResponse?>()
val userClips = _userClips as LiveData<UserClipsResponse?>
init {
viewModelScope.launch {
repository.authUser(this#TwitchViewModel)
}
}
fun currentUserId() = userData.value?.dataList?.get(0)?.id ?: ""
fun clipsListExists() = userClips.value != null
fun getUserByInput(input: String){
viewModelScope.launch {
_progressVisibility.value = View.VISIBLE
_userData.value = repository.getUserByName(input, this#TwitchViewModel)
_progressVisibility.value = View.GONE
}
}
/**
* #param userId The ID of the Streamer whose clips are gonna fetch. If null, resets
* If empty, sets the [userClips] value to null.
*/
fun getUserClips(userId: String){
if(userId.isEmpty()) {
_userClips.postValue(null)
return
}
if(userId == currentUserId() && _userClips.value != null) {
_userClips.postValue(_userClips.value)
return
}
viewModelScope.launch {
_userClips.value = repository.getUserClips(userId, this#TwitchViewModel)
}
}
}
When running the test with the normal ActivityRule and launching the Activity as it were a normal launch, the observers are triggering successfully.
I'm using a relaxed mock to avoid having to mock all functions and variables.
Finally found the problem and the solution with the debugger. Apparently, the #Before function call runs after the ViewModel is injected into the fragment, so even if the variables pointed to the same reference, mocked answer where executing only in the test context, not in the android context.
I changed the ViewModel initialization to the module scope like this:
#get:Rule
val fragmentRule = createRule(fragment, module {
single(override = true) {
makeMocks()
val twitchViewModel = mockViewModel()
twitchViewModel
}
})
private fun makeMocks() {
mockkStatic(Picasso::class)
}
private fun mockViewModel(): TwitchViewModel {
val userData = MutableLiveData<UserDataResponse>()
val twitchViewModel = mockk<TwitchViewModel>(relaxed = true)
every { twitchViewModel.userData } returns userData
every { twitchViewModel.getUserByInput("Rubius") }.answers {
updateUserDataLiveData(userData)
}
return twitchViewModel
}
And the Observer inside the Fragment got called!
Maybe it's not related, but I could not rebuild the gradle project if I have mockk(v1.10.0) as a testImplementation and as a debugImplementation.