I wanted to make a method that determine if the application is started for the very first time, no matter the current version of the application. People suggest that we should use SharedPreferences as seen from this qustion. Below is the function that determine if application is started for the very first time.
companion object {
const val APP_LAUNCH_FIRST_TIME: Int = 0 // first start ever
const val APP_LAUNCH_FIRST_TIME_VERSION: Int = 1 // first start in this version (when app is updated)
const val APP_LAUNCH_NORMAL: Int = 2 // normal app start
/**
* Method that checks if the application is started for the very first time, or for the first time
* of the updated version, or just normal start.
*/
fun checkForFirstAppStart(context: Context): Int {
val sharedPreferencesVersionTag = "last_app_version"
val sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context)
var appStart = APP_LAUNCH_NORMAL
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
appStart = when {
lastVersionCode == -1L -> APP_LAUNCH_FIRST_TIME
lastVersionCode < currentVersionCode -> APP_LAUNCH_FIRST_TIME_VERSION
lastVersionCode > currentVersionCode -> APP_LAUNCH_NORMAL
else -> APP_LAUNCH_NORMAL
}
// Update version in preferences
sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()
} catch (e: PackageManager.NameNotFoundException) {
// Unable to determine current app version from package manager. Defensively assuming normal app start
}
return appStart
}
}
Now in my MainActivity I make the check in this way, but strangely enough I always end up inside the if statement, although appLaunch is different from MainActivityHelper.APP_LAUNCH_FIRST_TIME
val appLaunch = MainActivityHelper.checkForFirstAppStart(this)
if (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) {
val c = 299_792_458L
}
Here we see that appLaunch is 2
Here we see that MainActivityHelper.APP_LAUNCH_FIRST_TIME is 0
I am in the main thread I check using Thread.currentThread(), and when I add watches in the debugger (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) I get false.
So I suggest that there is some delay, and by the time the if check is made the result is changed?
There's nothing wrong with the code. I tested it and it works as intended. I get all three return values depending on the circumstances. I simplified the code a bit but the original code should nevertheless works.
enum class AppLaunch {
LAUNCH_FIRST_TIME, // first start ever
FIRST_TIME_VERSION, // first start in this version (when app is updated)
NORMAL // normal app start
}
/**
* Method that checks if the application is started for the very first time, or for the first time
* of the updated version, or just normal start.
*/
fun checkForFirstAppStart(context: Context): AppLaunch {
val sharedPreferencesVersionTag = "last_app_version"
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
// Update version in preferences
sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()
when (lastVersionCode) {
-1L -> AppLaunch.LAUNCH_FIRST_TIME
in 0L until currentVersionCode -> AppLaunch.FIRST_TIME_VERSION
else -> AppLaunch.NORMAL
}
} catch (e: PackageManager.NameNotFoundException) {
// Unable to determine current app version from package manager. Defensively assuming normal app start
AppLaunch.NORMAL
}
}
I experimented a bit and the issue you see looks like a bug in Android Studio. If the code in the if statement is a NOP (no operation) then the debugger seems to stop there. If the code does have a side effect, the debugger doesn't stop.
Things like this can be infuriating but with Android, Android Studio and the tooling, bugs like this are pretty common (unfortunately).
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
val c = 299_792_458L
}
translates to the following byte code:
L3 (the if statement)
LINENUMBER 32 L3
ILOAD 4
IFNE L4
L5
LINENUMBER 33 L5
LDC 299792458
LSTORE 2
Converting c to a var
var c = 1L
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
c = 299_792_458L
}
results in identical byte code so it's certainly not a code problem but an issue with Android Studio.
Update
If you need fast writes with enums you can use something like this:
fun appLaunchById(id: Int, def: AppLaunch = AppLaunch.NORMAL) = AppLaunch.values().find { it.id == id } ?: def
enum class AppLaunch(val id: Int) {
LAUNCH_FIRST_TIME(0), // first start ever
FIRST_TIME_VERSION(1), // first start in this version (when app is updated)
NORMAL(2); // normal app start
}
^^^ writes an Int so fast and short. Reading is certainly not super fast though.
Update 2
Generic version of the enum solution:
inline fun <reified T : Enum<*>> enumById(hash: Int, def: T) = enumValues<T>()
.find { it.hashCode() == hash }
?: def
enum class AppLaunch {
LAUNCH_FIRST_TIME, // first start ever
FIRST_TIME_VERSION, // first start in this version (when app is updated)
NORMAL // normal app start
}
Usage:
val enum = enumById(value.hashCode(), AppLaunch.NORMAL)
Related
I'm very new to App development. I'm stuck on a piece of code that's telling me "The Expression is unused".
The App itself is written in Kotlin, it's meant to be a conversion app. I'm pretty sure I got the math right, since 1 foot comes out to 30.48cm, which is correct. But, whenever I write a number (in the app) greater than 1 foot, it still always comes to 30.48cm. An example being if I were to type 5foot 9 in the app, the answer would still be 30.48cm. Here is the two blocks of code I'm pretty sure one of them is the culprit.
The first one.
'calculateHeight' is the line thats giving me "The Expression is unused"
private fun calculateButton() {
val feetString: String = binding.editTextFeet.text.toString()
val inchesString: String = binding.editTextInches.text.toString()
val calculateHeight = calculateHeight()
if (feetString.isEmpty()) {
Toast.makeText(context, "Please select a foot value", Toast.LENGTH_SHORT).show()
} else {
calculateHeight
displayText()
}
if (inchesString.isEmpty()) {
Toast.makeText(context, "Please select a inch value", Toast.LENGTH_SHORT).show()
} else {
calculateHeight
displayText()
}
}
And the second one.
private fun calculateHeight(): Double {
val feetHint = binding.editTextFeet.toString()
val inchesHint = binding.editTextInches.toString()
var feet = 1
try {
feet = feetHint.toInt()
} catch (e: NumberFormatException) {
e.printStackTrace()
}
var inches = 0f
try {
inches = inchesHint.toInt().toFloat()
} catch (e: NumberFormatException) {
e.printStackTrace()
}
val totalFeet = feet * 12
val totalInches = inches + 1f
val heightInCentimeters = 2.54
return ((totalFeet * totalInches) * heightInCentimeters)
}
}
Edit
This is the displayText():
private fun displayText() {
val dec = DecimalFormat(".##")
val resultString: String = dec.format(calculateHeight())
binding.textViewCm.text = "$resultString - Centimeters"
}
val calculateHeight = calculateHeight()
This line is calling the calculateHeight() function. That function reads the contents of your fields at that moment in time and performs calculations upon their contents. The result of the calculateHeight() function is then stored in the variable, itself confusingly named calculateHeight.
You then do not do anything meaningful with calculateHeight. You reference that variable twice, in statements that then do not do anything:
calculateHeight
Those lines will give you "expression is unused", because the expression (calculateHeight) is unused. You are not doing anything with it.
it still always comes to 30.48cm
You do not state where and how you are seeing any results. If I had to guess, displayText() is supposed to something like that, given the name of that function. Your question does not include the source for displayText(), so we are having to guess.
But, it is unclear where displayText() is getting anything to display. You are not passing any parameters to displayText(). Perhaps you should have displayText(calculateHeight), and have your displayText() function take that parameter and do something with it. Or, perhaps displayText() should be calling calculateHeight() directly, and you can remove all the calculateHeight()/calculateHeight stuff from your calculateButton() function.
I have a counter logic using Flow in ViewModel, and auto increment.
class MainViewModel(
private val savedStateHandle: SavedStateHandle
): ViewModel() {
val counterFlow = flow {
while (true) {
val value = savedStateHandle.get<Int>("SomeKey") ?: 0
emit(value)
savedStateHandle["SomeKey"] = value + 1
delay(1000)
}
}
}
In the Activity
val counterFlowStateVariable = viewModel.externalDataWithLifecycle.collectAsStateWithLifecycle(0)
This counter will only increment and count during the App is active
It stops increment when onBackground, and continues when onForeground. It doesn't get reset. This is made possible by using collectAsStateWithLifecycle.
It stops increment when the Activity is killed by the system and restores the state when the Activity is back. The counter value is not reset. This is made possible by using savedStateHandle
I'm thinking if I can use a stateFlow instead of flow?
I would say, you should. flow is cold, meaning it has no state, so previous values aren't stored. Because your source of emitted values is external (savedStateHandle) and you mix emitting and saving that value within the flow builder, you introduce a synchronization problem, if more than one collector is active.
Perform small test:
// this value reflects "saveStateHandle"
var index = 0
val myFlow = flow {
while(true) {
emit(index)
index++
delay(300)
}
}
Now collect it three times:
launch {
myFlow.collect {
println("First: $it")
}
}
delay(299)
launch {
myFlow.collect {
println("second: $it")
}
}
delay(599)
launch {
myFlow.collect {
println("third: $it")
}
}
You'll start noticing that some collectors are reading previous values (newer values already read by other collectors), meaning their save operation will use that previous value, instead up to date one.
Using stateFlow you "centralize" the state read/update calls, making it independent of a number of active collectors.
var index = 0
val myFlow = MutableStateFlow(index)
launch {
while (true) {
index++
mySharedFlow.value = index
delay(300)
}
}
I have a ViewModel in android and I am trying to validate whether the person has entered his name and age before moving on to the next page.
Here is my code:
fun onContinueClick() {
val navigateNextPage: (Int) -> Unit = lambda#{ validation ->
if (validation < 2)
return#lambda
nextPageUseCase().onEach {
_navigationNotify.value = it // Moving on to the next page
}
}
viewModelScope.launch {
var valid = 0
getName().collect { name ->
if (name != null) navigateNextPage(++valid)
}
getAge().collect { age ->
if (age != null) navigateNextPage(++valid)
}
}
}
Although this is working as expected, is it efficient to perform this operation? I think that if both the ++valid happen at the same time, I wouldn't be able to go to the next page.
I want to know how to synchronize the code to avoid that situation.
I have an app with a splashscreen, which stays for about 2 seconds.
After that, it switches to another activity A.
In A, I set a value in a SeekBar and after that, click a Button to confirm.
When I simply start a recorded Espresso test doing this, it tries to play while on the splashscreen. So when it tried to set the SeekBar value or click the Button, I get a NoMatchingViewException. So my first attempt at fixing this was to simply add a sleep(5000). This worked.
However, I don't want to put a manual sleep in after every Activity switch.
Because it seems like unnecessary code
Because it would mean unnecessary waiting time for running the test
The timing might be arbitrary and could be different for different devices
So I tried to check whether or not I'm in the right Activity/can see the right views. I did this using some SO links: Wait for Activity and Wait for View.
However, even that does not work 100%.
I have these two functions:
fun <T: AppCompatActivity> waitForActivity(activity: Class<T>, timeout: Int = 5000, waitTime: Int = 100) {
val maxTries = timeout / waitTime
var tries = 0
for(i in 0..maxTries) {
var currentActivity: Activity? = null
getInstrumentation().runOnMainSync { run { currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(
Stage.RESUMED).elementAtOrNull(0) } }
if(activity.isInstance(currentActivity)) {
break
} else {
tries++
sleep(waitTime.toLong())
}
}
}
fun waitForView(
#IntegerRes id: Int,
waitMillis: Int = 5000,
waitMillisPerTry: Long = 100
): ViewInteraction {
// Derive the max tries
val viewMatcher = allOf(
withId(id),
isDisplayed()
)
val maxTries = waitMillis / waitMillisPerTry.toInt()
var tries = 0
for (i in 0..maxTries)
try {
tries++
val element = onView(viewMatcher)
element.check { view, noViewFoundException ->
if(view == null) {
throw noViewFoundException ?: Exception("TEST")
}
if(view.hasWindowFocus()) {
throw noViewFoundException ?: Exception("TEST2")
}
}
return element
} catch (e: Exception) {
if (tries == maxTries) {
throw e
}
sleep(waitMillisPerTry)
}
throw Exception("Error finding a view matching $viewMatcher")
}
Neither of those work 100%. Both of them seem to return within the timeout restrictions, and have "found" the activity/view. However, the expected view, e.g. a Button is not yet ready to perform, for example, element.perform(click()). It does not lead to a NoMatchingViewException, but it does not perform the click I did either. For the SeekBar, I use this:
private fun setSeekValue(seekBar: ViewInteraction, age: Int) {
val fullPercentage = .9f
val step = 1/99f
seekBar.perform(
GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val pos = IntArray(2)
view?.getLocationOnScreen(pos)
FloatArray(2).apply {
this[0] = pos[0] + view!!.width * (.05f + fullPercentage*step*age)
this[1] = pos[1] + view.height * .5f
}
},
PrecisionDescriber {
FloatArray(2).apply {
this[0] = .1f
this[1] = 1f
}
},
InputDevice.SOURCE_MOUSE,
MotionEvent.ACTION_BUTTON_PRESS
)
)
}
However, when I use these functions and just put a very short sleep, e.g. sleep(100) after it, it works. This again however, would go against the three reasons listed above, which I'm trying to avoid here.
As you can see in the function waitForView, I tried to check if the View is "usable", using hasWindowFocus(). But this still does not perform the click, except for when I again put a sleep(80) or something after it. So it waits for the splashscreen to switch to A, finds the view it's looking for and then can't perform the click, except for when I wait a little bit.
I have also tried these functions of View:
isEnabled
isShown
visibility
getDrawingRect
isFocusable
isFocused
isLayoutDirectionResolved
Neither of them worked as I expected. With all of them, after the needed value was returned on the element.check part of waitForView, they would still not be accessible without putting a short sleep after.
Is there a way to reliably check if I can perform a click on a view/safely can perform ViewInteraction.perform()
Either by checking, if an activity is fully loaded to a point where its views are usable. Or by directly checking if a view is usable.
Is there any way of being notified if a discovered BLE peripheral moves out of range or otherwise drops out of sight? I'm using rxBleClient.scanBleDevices() to build a list of devices in the area that are advertising, but before shipping this list to the main application I'd like to be sure that all the devices are still reachable. What's the best way of doing this?
The vanilla Android Scan API allows for scanning BLE devices with callback types of:
/**
* A result callback is only triggered for the first advertisement packet received that matches
* the filter criteria.
*/
public static final int CALLBACK_TYPE_FIRST_MATCH = 2;
/**
* Receive a callback when advertisements are no longer received from a device that has been
* previously reported by a first match callback.
*/
public static final int CALLBACK_TYPE_MATCH_LOST = 4;
The same API is available via RxBleClient.scanBleDevices(ScanSettings, ScanFilter...)
The CALLBACK_TYPE_FIRST_MATCH and CALLBACK_TYPE_MATCH_LOST are flags that can be put into ScanSettings.
The timeout after which the CALLBACK_TYPE_MATCH_LOST is triggered is somewhere around 10 seconds. This may be an indication that a particular device is no longer in range/available.
You can create a Transformer that will collect scanned devices and emit a list, that is kept up to date, depending on how long ago the device was recently seen.
Robert, that may not be exactly what you expect but treat it as an example. My Transformer is emitting a list of items whenever it has been changed, either because an update from the scanner or the eviction happened (checked every second).
class RollingPairableDeviceReducer(
private val systemTime: SystemTime,
private val evictionTimeSeconds: Long,
private val pairableDeviceFactory: PairableDeviceFactory
) : Observable.Transformer<ScannedDevice, List<PairableDevice>> {
override fun call(source: Observable<ScannedDevice>): Observable<List<PairableDevice>> {
val accumulator: MutableSet<PairableDevice> = Collections.synchronizedSet(mutableSetOf())
return source
.map { createPairableDevice(it) }
.map { pairableDevice ->
val added = updateOrAddDevice(accumulator, pairableDevice)
val removed = removeOldDevices(accumulator)
added || removed
}
.mergeWith(checkEvictionEverySecond(accumulator))
.filter { addedOrRemoved -> addedOrRemoved == true }
.map { accumulator.toList() }
}
private fun createPairableDevice(scannedDevice: ScannedDevice)
= pairableDeviceFactory.create(scannedDevice)
private fun updateOrAddDevice(accumulator: MutableSet<PairableDevice>, emittedItem: PairableDevice): Boolean {
val existingPairableDevice = accumulator.find { it.deviceIdentifier.hardwareId == emittedItem.deviceIdentifier.hardwareId }
return if (existingPairableDevice != null) {
accumulator.remove(existingPairableDevice)
existingPairableDevice.updateWith(emittedItem)
accumulator.add(existingPairableDevice)
false
} else {
accumulator.add(emittedItem)
true
}
}
private fun checkEvictionEverySecond(collector: MutableSet<PairableDevice>): Observable<Boolean>
= Observable.interval(1, TimeUnit.SECONDS)
.map { removeOldDevices(collector) }
private fun removeOldDevices(accumulator: MutableSet<PairableDevice>): Boolean {
val currentTimeInMillis = systemTime.currentTimeInMillis()
val evictionTimeMillis = TimeUnit.SECONDS.toMillis(evictionTimeSeconds)
return accumulator.removeAll { (currentTimeInMillis - it.lastSeenTime) >= evictionTimeMillis }
}
}