Office Hours Transcript: 2021-12-03

john joined

hello, john!

 

how can I help you today?

Hello Mark,

There is a slight issue my viewmodel.

I made an app that loads a number of excerpts (blocks of text). The same excerpt appears on both HomeScreen (the startDestination navigation of app) and BrowseExcerptsScreen, to which you can navigate from HomeScreen. When I change the excerpt in BrowseExcerptsScreen and move back to HomeScreen using the back arrow, the HomeScreen is still displaying the old excerpt. However, if I close the app and reopen it, then the correct excerpt is display (I am using data store to persist the excerpt),

 

Is my explanation clear enough?

I do not know, in part because I am uncertain what your question is.

 

how will the viewmodel for HomeScreen find out about the changes that you make in BrowseExcerptsScreen?

My question is: why is the new excerpt not persisted when I navigate to HomeScreen using the back arrow?

 

The ViewModel is shared.

do you mean that the viewmodel class is shared, or that the viewmodel instance is shared?

That’s a very good question. I think it is the instance. Please let me show you the code

 

@Composable
fun ExcerptScreen(viewModel: ExcerptVM = hiltViewModel()) {

val currentExcerpt = viewModel.currentExcerptOriginal.collectAsState().value

Text(text = currentExcerpt,
    textAlign = TextAlign.Right,
    fontSize = 20.sp,
)

}

=======================================-
HOMESCREEN

fun HomeScreen(navController: NavController) {

ExcerptScreen()

            ...
            
                    Button(onClick = { navController.navigate(Screen.Settings.route) }) {
        Text(text = "Settings")

    }


}

==================================================

 

BROWSESCREEN

@Composable
fun BrowseExcerptScreen(viewModel: ExcerptVM = hiltViewModel()) {

        ExcerptScreen()
        
        ...
        Icon(
            painter = painterResource(id = R.drawable.ic_round_arrow_left_24),
            contentDescription = "Back",
            modifier = Modifier
                .clickable { viewModel.previousExcerpt() }
                .size(64.dp)
        )

        Icon(
            painter = painterResource(id = R.drawable.ic_round_arrow_right_24),
            contentDescription = "Forward",
            modifier = Modifier
                .clickable { viewModel.nextExcerpt() }
                .size(64.dp)
        )

    }

}
 

VIEWMODEL

@HiltViewModel
class ExcerptVM @Inject constructor(
private val dataStoreRepository: DataStoreRepository,
private val repository: ExcerptRepository
) : ViewModel() {

private val _allExcerpts = MutableStateFlow<List<Excerpt>>(listOf())

private val _currentExcerptPosition = MutableStateFlow(0)

private val _currentExcerptOriginal = MutableStateFlow("")
val currentExcerptOriginal : StateFlow<String> = _currentExcerptOriginal

so, these are two composable screens, where you are using Navigation for Compose to navigate between them – correct?

Correct.

then AFAIK those are separate viewmodel instances

 

For example, if ExampleScreen is a destination in a navigation graph, call hiltViewModel() to get an instance of ExampleViewModel scoped to the destination

(emphasis added)

 

each navigation destination gets its own viewmodel instance, much like how fragments each get their own viewmodel instance

 

note that I have not used your specific combination of technologies, so I may be missing something

 

you might consider logging your ExcerptVM instances to see if they are indeed the same instance or not – my guess, given your symptoms, is that they are not

logging your ExcerptVM

How do you do this? viewModel.toSring()?

yes, or Kotlin string interpolation: Log.d(TAG, "ExcerptVM in HomeScreen: $viewModel")

 

since this is an ordinary Kotlin class, unless you wrote a toString() somewhere, it should inherit the base one, which will print the object ID as part of its output

I see. And what would the best way to fix it be?

I cannot really answer that. My tendency is to prefer smaller-scoped viewmodels, like what I think you wound up with. If so, the I would focus on a reactive way for a viewmodel instance to find out about data changes.

 

(sorry, that should be "then I would focus…")

No problem.

a reactive way for a viewmodel instance to find out about data changes.

Would you mind briefly explaining this?

I have not used DataStore, so I do not know what sort of data-change listener mechanism it might have. But, a viewmodel should be able to be told when you change data in your DataStore.

Got it.

 

Next, I have a bug that is probably hardware related, but I thought I’d ask you just in case. I’m trying to use AlarmClock to triggered the phone native alarm.

I works fine on if I just use the default ringtone, but it crashes if I a select a song for the ringtone. I don’t think it’s a Uri problem because it seems to be working fine on Android 7.1 (the crash happens on Android 8.1)

 
    val regularAlarmIntent = Intent(AlarmClock.ACTION_SET_ALARM)
    regularAlarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, true)
    regularAlarmIntent.putExtra(AlarmClock.EXTRA_HOUR,individualTimes.time.hour)
    regularAlarmIntent.putExtra(AlarmClock.EXTRA_MINUTES,individualTimes.time.minute)
    regularAlarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, individualTimes.timeOfDay.toString())
    regularAlarmIntent.putExtra(AlarmClock.EXTRA_RINGTONE, filePath.value.toString())
    regularAlarmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

    val extras = regularAlarmIntent.getExtras()
    startActivity(app,regularAlarmIntent,extras)

what is the value of EXTRA_RINGTONE, where did it come from, and what is the exception that you are getting?

I select the song in a SettingsScreen composable using ActivityResultContracts.StartActivityForResult()

An example of the value of EXTRA_RINGTONE is content://com.android.providers.media.documents/document/audio%3A392

As for the exception, well there is none… When the Alarm gets triggered, it uses another ringtone, not what I selected (not the same as the default one, but still something to the phone).

Also, if I check the Alarm settings of the phone, I can see that a new Alarm was created. If I use the default ringtone (I don’t select a song), I am able to edit the alarm manually. But If I do a select a song, I am unable to open the edit screen (the phone just exits the Alarm settings app)

 

On Android 7.1, the new alarm does not appear in the Alarm settings (which is actually what I want)

I don’t know that the Alarm Clock app necessarily has the rights to access the content identified by the Uri. What Intent are you using for StartActivityForResult()?

On Android 7.1, the new alarm does not appear in the Alarm settings (which is actually what I want)

That is not strictly related to OS version, but rather whatever app handles ACTION_SET_ALARM, which could vary by device model.

Intent(AlarmClock.ACTION_SET_ALARM)

No, sorry, I mean what you were referring to in "I select the song in a SettingsScreen composable using ActivityResultContracts.StartActivityForResult()".

 

In other words, what are you using to get that Uri?

intentSelectSong = Intent()
.setType("audio/*")
.setAction(ACTION_OPEN_DOCUMENT)
.setFlags(FLAG_GRANT_READ_URI_PERMISSION)
.setFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION)

the Alarm Clock app will not have access to that content via that Uri

 

and AFAIK you do not have the ability to transfer permission with SET_ALARM_CLOCK

And that’s only on Oreo and above?

it should be for Android 4.4 and above – I cannot explain how it is working for you on 7.1

Does it mean the only fix is to have a local copy of the song in my app?

effectively, you need to use an unsecured Uri, one that every single app on the system can read from

 

you might try switching to ACTION_PICK and choosing a suitable MediaStore Uri to pick from

 

otherwise, I think you will need to implement your own ContentProvider, one that either reads from a copy of the song that you have as a local file, or one that somehow serves as a proxy to the content at your desired Uri

 

(or do not use AlarmClock, or do not use custom ringtones)

 

you might also look to see what people have been doing for custom Notification ringtones using user-selected media, as we have been having similar issues there since at least 7.0

OK, let me think about it. Thank you very much for your time!

happy to help!

john left