Screen Flashes when using datastore to update UI - android

I am using Jetpack Compose to build my UI and when a user enters the app, the app will first check whether he/she is a first time user.
If it's a first time user, it will load the ScreenStarter() composable. Else, it will load the AppContent() composable.
My code is like that:
App.kt
{
/**
* This is the Main MES app that will
* determine which screen content to show
**/
/** Load the app settings from datastore **/
val appSettings = application.datastore.data.collectAsState(initial = MesAppSettings()).value
/** Set the correct app theme that the user has set **/
val darkTheme = when (appSettings.appTheme) {
AppTheme.FOLLOW_SYSTEM -> isSystemInDarkTheme()
AppTheme.DARK -> true
AppTheme.LIGHT -> false
}
MesTheme(
darkTheme = darkTheme // Load the app theme
) {
/** Determine screen content **/
if (!appSettings.isFirstTimeLogging) {
AppContent(
application = application,
appSettings = appSettings,
widthSizeClass = widthSizeClass
)
} else {
ScreenStarter(
application = application,
requestMultiplePermissions = requestMultiplePermissions
)
}
}
}
The issue here is that, if it is a recurrent user and he/she opens the app, the screen flashes, and we can briefly see the ScreenStarter() composable before it switches to the AppContent() composable. I think this happens because the data is fetched asynchronously from the data store.
Can someone advise on how to fix this ?

Use some different initial value for appSettings. You can use null for example, then if appSettings is null, show blank screen or some progress indicator, if it's not null, you know that correct data has been loaded from storage and you can show content based on your logic.

Related

How to handle configuration changes while working with Jetpack compose and Kotlin flows

I've been working with flows and jetpack compose, I've been trying to fix it for the past day. I've gone through a few articles, and everything works as expected unless the device is rotated or switched to dark mode. I've placed the actual flow stuff in the viewmodel and fetched it from the composable function with .collectAsState(); I've also tried with .collectAsStateWithLifecycle(), which kind of collects the flow's data with life-cycle awareness (it's still experimental). But the problem still remains.
At this point:
in HomeScreenViewModel.kt
val issLocationFromAPIFlow = mutableStateOf<Flow<ISSLocationDTO>>(emptyFlow())
...
init{
viewModelScope.launch(Dispatchers.Default + coroutineExceptionalHandler) {
val issLocationData = async { issLocationFetching.getISSLatitudeAndLongitude() }
issLocationFromAPIFlow.value = issLocationData.await()
}
}
in Composable.kt
#Composable
fun ISSData() {
val homeScreenViewModel: HomeScreenViewModel = viewModel()
val issLatitude = homeScreenViewModel.issLocationFromAPIFlow.value.collectAsStateWithLifecycle(
initialValue = ISSLocationDTO(IssPosition("", ""), "", 0)
).value.iss_position.latitude
Text(text = issLatitude, color = AppTheme.colors.primary)
}
This works as expected. If I didn't change it to dark mode or rotate the device, but at some point when I rotate or switch to dark mode, the value falls to null, and it just can't handle it, even though I've placed an actual data source in the viewmodel.
I read this article, which mentions storing the flow from the viewmodel and a local lifecycle owner [LocalLifecycleOwner] as a key in remember, as far as I know, that remember avoids recomposition and sort of caches and returns the cached value from variables or whatever, but in my case, I want the newly updated data through the flow, which should handle device configuration changes.
Screen recordings of :
while rotating the device
while switching to dark theme or vice-versa
Does anyone know how to fix this problem??
Thank you.
Try to simplify little bit like:
val issLocationFromAPIFlow : Flow<ISSLocationDTO>> =
issLocationFetching.getISSLatitudeAndLongitude()
.onStart{
emit(ISSLocationDTO(IssPosition("", ""), "", 0))
}.flowOn(Dispatchers.Default)
...
init{}
then
val homeScreenViewModel: HomeScreenViewModel = viewModel()
val issLatitude = homeScreenViewModel.issLocationFromAPIFlow
.collectAsState(null).value?.iss_position?.latitude
make sure that issLocationFetching.getISSLatitudeAndLongitude() is not a suspending function, it should just return a flow

Proper way to go from one Activity to another Activity within the same App using UIAutomator

I am trying to write some UI tests with UIAutomator on Android. I need to use UIAutomator to perform the following actions:
Start the app, wait for the page to be fully loaded.
Click the button to go to another activity. --> This is where I am stuck, I am trying to make it wait until another activity finish rendering (and wait for step 3).
Perform UI tests on the designated activity.
Could anyone please give me an example ? Thanks!
UiDevice & UiObject2 tests classes offer arbitrary timeout wait period associated to the matching condition before proceeding in tests.
Notice that the timeout value is the maximum amount of time to wait in milliseconds before declaring that the condition is not met; so it doesn't sleep your test until the timeout period expires; instead it tries to match the condition (finding a matched component for instance) and once the condition is met, the test continues without waiting the expiration of the timeout value.
So, you can assign an arbitrary value that can make sure that the test succeeds. So, it's safe if you want to set it to Long.MAX_VALUE; but make sure that should succeed to avoid ANR. In the below example, I am using 500 milliseconds.
For instance to launch an app, and wait for it to appear as per documentation:
var device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
....
// Wait for the app to appear
device.wait(
Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), // condition
LAUNCH_TIMEOUT // timeout
)
}
Similarly, you can wait/timeout until you can find the target component at the second activity, before proceeding in the test; something like:
val someView = mDevice.wait(
Until.findObject(
By.res( // find object by resource id
BASIC_SAMPLE_PACKAGE, // application package name
"myViewId" // id of the view in the second activity
)
),
500) /* wait 500ms */
So, in your test that you are trying to do, you need to:
Use the #Before test method (that precedes any test) to make sure the app is launched and its main actvivity is shown:
private lateinit var mDevice: UiDevice
private val BASIC_SAMPLE_PACKAGE = "com.example.android......" // change this to your app's package name
private val LAUNCH_TIMEOUT = 5000L
#Before
fun startMainActivityFromHomeScreen() {
// Initialize UiDevice instance
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// Start from the home screen
mDevice.pressHome()
// Wait for launcher
val launcherPackage = getLauncherPackageName()
assertThat(launcherPackage, CoreMatchers.notNullValue())
mDevice.wait(
Until.hasObject(By.pkg(launcherPackage).depth(0)),
LAUNCH_TIMEOUT
)
// Launch the blueprint app
val context = ApplicationProvider.getApplicationContext<Context>()
val intent = context.packageManager
.getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE)
intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) // Clear out any previous instances
context.startActivity(intent)
// Wait for the app to appear
mDevice.wait(
Until.hasObject(
By.pkg(BASIC_SAMPLE_PACKAGE)
.depth(0)
),
LAUNCH_TIMEOUT
)
}
Then to press on a button to launch the second activity:
// searching for a UI component with a resource Id btn_goto_second
val secondActivityButton = mDevice.wait(
Until.findObject(
By.res(
BASIC_SAMPLE_PACKAGE,
"btn_goto_second" // change to your button id
)
),
500 /* wait 500ms */
)
// Perform a click on the button to load the second activity.
secondActivityButton.click()
Then to enter some text to an EditText at the second activity; you can just wait until you find this view; and then type some text:
// searching for the EditText component with a resource Id edit_text
val editText = mDevice.wait(
Until.findObject(
By.res(
BASIC_SAMPLE_PACKAGE,
"edit_text" // change this to yoru editText id
)
),
500 /* wait 500ms */
)
// Set the text to the EditText
editText.text = "some text"
And to verify the text result, use one of the assertion methods:
assertThat(
editText.text,
CoreMatchers.`is`(equalTo("some text"))
)
You can check the documentation for further help; or refer to their sample app. Also this is a nice repo that would help.

Separate background thread pointing on an old ViewModel after restating app

I have an app that do BLE scan on the background and alert the user by playing a sound and shows up a popup with a button to snooze. So if the user click on that button the app will stop playing the alert sound and dismiss the popup for a while (default duration is one minute)
So if the app in background mode it still works and can play the alert sound and then when I reopen the app by clicking on the service notification it shows the popup
So I'm calculating the spent time after the user click on dismiss button on a separate thread using some variables in the viewModel because I need them in other place.
Here is my code
Thread {
while (true) {
val previous = tagDetailsAlertsViewModel.stopAlertingAt.value
val durationMin = PreferenceManager.getDefaultSharedPreferences(this).getString("dismiss_duration", "1")!!.toInt()
val diff = (Date().time - previous!!.time) / 1000 / 60
if ( diff >= durationMin && tagDetailsAlertsViewModel.isStopAlerting() ) {
tagDetailsAlertsViewModel.setStopAlerting(false)
}
}
}.start()
The code above is to check always if the duration past to alert again the user
binding.btnDismiss.setOnClickListener {
tagDetailsAlertsViewModel.stopAlertingAt.value = Date()
tagDetailsAlertsViewModel.setStopAlerting(true)
}
So the problem here is when I close the app and open it again the line code below will point always on the last value before I close the app
val previous = tagDetailsAlertsViewModel.stopAlertingAt.value
But here it change correct the value
tagDetailsAlertsViewModel.stopAlertingAt.value = Date()
So if I understood correctly is that the background thread still use the mode ViewModel with the old value and I can't figure out how to fix that.
I'm using Koin Module for my viewModels

Is there a way to programmatically perform input action on system permission dialogs on Android?

This request is for a specific use case of a product
Assuming I have an AccessibilityService instantiated as part of my application's context: I need to continuously capture screen content, for which i'm using a MediaProjectionManager attached to my application. This is working, with a caveat: every time I restart my device or app, is mandatory to make a new request for an instance of MediaProjection, forcing the user to accept the screen capture system permission dialog.
I am trying to skip this step for the user. I already achieved automatically firing the dialog request. But for accepting the dialog prompt, this is what I've tried:
Log view hierarchy and find the nodeViewId of the corresponding
dialog accept button. Problem: the AccessibilityService wasn't providing an
event.source for that system dialog.
Using AccessibilityService.dispatchGesture to perform a click action
on the coordinate of the "Start Now" button of the dialog, after the
system shows it.
// PermissionAutomator
permissionsAutomator.listener = object : PermissionsAutomator.Listener {
override fun onPerformClickOnCoordinate() {
val clickPath = Path()
clickPath.moveTo(604F, 737F)
val clickStroke = StrokeDescription(clickPath, 0, 1)
val clickBuilder = GestureDescription.Builder()
clickBuilder.addStroke(clickStroke)
val gesture = clickBuilder.build()
val callback = object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription) {
super.onCompleted(gestureDescription)
Timber.d("gesture onCompleted")
}
override fun onCancelled(gestureDescription: GestureDescription) {
super.onCancelled(gestureDescription)
Timber.d("gesture onCancelled")
}
}
dispatchGesture(gesture, callback, null)
}
}
Does anyone has some advice for this problem? Maybe the solution can be extended for every system permission dialog on Android. Thanks!
System permission dialog for MediaProjection
Looking at the MediaProjectionManager https://developer.android.com/reference/android/media/projection/MediaProjectionManager
it seems its built-in to ask every time. Its not a regular permission like READ_EXTERNAL_STORAGE that is saved. The only way I see around it is to make your own and since MediaProjectionManager and MediaProjection are final it would have to be from scratch if its even possible.

change default preference on upgrade

In my app, I have the following code that tells me if a feature is enabled by default :
public boolean getFeatureEnabled()
{
return mPrefs.getBoolean("FEATURE_ENABLED", DEFAULT_ENABLED);
}
This preference is overwritten only when the user changes the setting from UI. So by default it draws its value from DEFAULT_ENABLED which is a class variable somewhere.
In the current version, DEFAULT_ENABLED is true but on the next version of my app will be false.
The problem is that after the update, with the above code the old users who did not change the default setting from UI will have their feature disable - and I want to avoid this.
Any advices on how to handle this ?
As I understand, you have a feature that was enabled by default but this default was never written to SharedPreferences unless explicitly changed by the user.
Now you want the feature to be disabled by default but without affecting the behavior for users that already have it enabled.
I can think of 3 options:
Option 1 If you are already saving the last version, you could check that in your migration logic:
private void migratePreferences(Context context) {
SharedPreferences prefs = context.getSharedPreferences("your_preference_file", MODE_PRIVATE);
int lastKnownVersionCode = (prefs.getInt("LAST_INSTALLED_VERSION", BuildConfig.VERSION_CODE);
prefs.edit().putInt("LAST_INSTALLED_VERSION", BuildConfig.VERSION_CODE).apply();
//this is the old featureEnabled check
boolean oldPreferenceValue = prefs.getBoolean("FEATURE_ENABLED", true);
boolean newPreferenceValue;
if (prefs.contains("FEATURE_ENABLED")) {
//the feature was modified by the user so respect their preference
newPreferenceValue = prefs.getBoolean("FEATURE_ENABLED", false);
} else if (lastKnownVersionCode == BUGGY_VERSION_WITH_FEATURE_ENABLED_BY_DEFAULT) {
//the user is updating from the buggy version.
// this check could include a range of versions if you've released several buggy versions.
// this is also where option 2 would be inserted
newPreferenceValue = oldPreferenceValue;
} else {
//the new default that will apply to fresh installs
newPreferenceValue = false;
}
//save the preference
prefs.edit().putBoolean("FEATURE_ENABLED", newPreferenceValue).apply();
}
This, however depends on your already having a call to this method somewhere in your app startup code.
Option 2 In case you don't, there is still hope. You can check if this is your first install using the answers given in this StackOverflow answer
Option 3 You can release an intermediate version of your app that behaves as it does now but saves the unsaved default setting in SharedPreferences. This will keep the feature AS IS for your eager users but you will have to wait until a significant portion of users updates before releasing the desired behavior.
Put another flag "FIRST_TIME" as "true" in your preferences in new build. Check on the very first screen of your app
if(FIRST_TIME==true)
{
//put FEATURE_ENABLED = false;
//put FIRST_TIME = false;
}
By doing this FEATURE_ENABLED will set to false for the first time the user launches the app and will not consider the default value

Categories

Resources