Avoid fragment recreation when opening from notification navigation component - notifications

I want when I click on a notification to open a fragment and not recreate it. I am using navigation component and using NavDeepLinkBuilder
val pendingIntent = NavDeepLinkBuilder(this)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.workouts_graph)
.setDestination(R.id.workoutFragment)
.createPendingIntent()
My case is I have a fragment and when you exit the app, there is a notification which when you click on it, it should return you to that same fragment. Problem is every time i click on it it's creating this fragment again, I don't want to be recreated.

I had the same issue. Looks like there is not an option to use the NavDeepLinkBuilder without clearing the stack according to the documentation
I'm not sure the exact nature of your action, but I'll make two assumptions:
You pass the destination id to your MainActivity to navigate.
Your MainActivity is using ViewBinding and has a NavHostFragment
You will have to create the pending intent like:
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("destination", R.id.workoutFragment)
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
And in your MainActivity, you can handle both cases (app was already open, app was not already open)
override fun onStart() {
super.onStart()
// called when application was not open
intent?.let { processIntent(it) }
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// called when application was open
intent?.let { processIntent(it) }
}
private fun processIntent(intent: Intent) {
intent.extras?.getInt("destination")?.let {
intent.removeExtra("destination")
binding.navHostFragment.findNavController().navigate(it)
}
}

Related

Jetpack Compose not updating / recomposing Flow List Values from Room DB when DB Data is getting changed

I'm trying to show a List of Items in my Android App. I'm using Jetpack Compose, Flows and RoomDB.
When launching the Activity all Items are shown without any problems, the Flow get's items collected and they are displayed.
But when I change some properties of the Item in the Database, the changes are not displayed. In my case I change the item to deleted, but it's still displayed as not deleted.
When I look at the Database Inspector, the value is changed in the database and set to deleted.
When I log collecting the flow, the change is getting emitted (It says the Item is set to deleted)
But Jetpack Compose is not recomposing the change.
If I delete an element from / add an element to the List (in the DB) the UI gets updated and recomposed.
So I can only assume that the problem must lie in the recomposition or handling of the flow.
Here my Code:
My Activity:
#AndroidEntryPoint
class StockTakingHistoryActivity : ComponentActivity() {
private val viewModel: StockTakingHistoryViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.stockList = ...
setContent {
LaunchedEffect(Unit) {
viewModel.getStockListItems(viewModel.stockList!!.uuid)
}
Surface(color = MaterialTheme.colors.background) {
Content(viewModel.stockListItems)
}
}
}
}
...
#Composable
private fun Content(items: List<StockListItem>) {
...
LazyColumn {
items(items) { item ->
HistoryItem(stockListItem = item)
}
}
}
}
...
#Composable
private fun HistoryItem(stockListItem: StockListItem) {
...
Text(text = stockListItem.deleted)
...
Button(onClick = {
viewModel.deleteItem(stockListItem)
}) {
Text(text = "Set to deleted!")
}
}
}
My ViewModel:
var stockListItems by mutableStateOf(emptyList<StockListItem>())
fun getStockListItems(uuid: String) {
viewModelScope.launch {
stockListItemRepository.findByUUID(uuid).collect { items ->
Log.d("StockTakingHistoryViewModel", "items changed! ${items.map { it.deleted }}")
stockListItems = items
}
}
}
fun deleteItem(stockListItem: StockListItem) {
viewModelScope.launch(Dispatchers.IO) {
stockListItemRepo.update(item.copy(deleted = true);
}
}
The Repository:
fun findByUUID(uuid: String): Flow<List<StockListItem>> {
return dao.findByUUID(uuid)
}
The Dao behind the Repository Request:
#Query("select * from StockListItem where stockListUUID = :uuid order by createdAt desc limit 30")
fun findByUUID(uuid: String): Flow<List<StockListItem>>
I would be very happy if someone could help me! Thank you!
Considering you can collect a flow as state (via collectAsState) I'd consider going that route for getting the list rather than calling collect in the viewModel and updating the stockListItems as there are fewer moving parts for things to go wrong.
For example something like the following:
setContent {
val stockListItems = viewModel.getStockListItemsFlow(uuid).collectAsState(initial = emptyList())
Surface(color = MaterialTheme.colors.background) {
Content(stockListItems)
}
}
Found the Problem: The equals() method of StockListItem only compared the primary key.

Kotlin app using camera does not return picture if camera is never launched before

I've stuck for a while with annoying issue with emulator's camera in my app. (The same is repro on a real device)
The camera is used from a dialog fragment to take a picture that later will be uploaded to remote host.
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
makeImageWithCamera()
} else {
dismiss()
}
}
private val cameraLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { isSuccess ->
if (isSuccess) {
path?.let { path ->
viewModel.setPhoto(path = path)
}
}
dismiss()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeFlow(viewModel.action) { dismiss() }
with(binding) {
buttonCamera.setOnClickListener { useCamera() }
buttonCancel.setOnClickListener { dismiss() }
}
}
private fun useCamera() {
val isGranted = ContextCompat.checkSelfPermission(requireContext(), PERM_CAMERA) ==
PackageManager.PERMISSION_GRANTED
if (!isGranted) {
cameraPermissionLauncher.launch(PERM_CAMERA)
} else {
makeImageWithCamera()
}
}
private fun makeImageWithCamera() {
try {
val photoFileName = "photo${id}"
val photosDir = requireContext()
.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val photoFile = File.createTempFile(photoFileName, ".jpg", photosDir)
val photoUri: Uri = FileProvider.getUriForFile(
requireContext(),
"app.provider",
photoFile
)
path = photoFile.absolutePath
cameraLauncher.launch(photoUri)
} catch (err : IOException) {
Log.e(TAG, err.stackTraceToString())
}
}
The issue happens only in situation when camera itself is never launched on a device:
Inside my app I press camera button
Receive a request for camera usage permission from my app and accept it.
Camera app is starting and requests its own permission to access geolocation (it wants to add location-based tags to photos) - at this point it is irrelevant wether to accept or deny it.
Camera enters the scene. Now if I press shot button it makes a photo and stays in the scene allowing to make infinite amount of photos, as if I've launched Camera app in regular way.
If I use back button to return to my app - nothing have change at all, dialog is just dismisses.
If I launch the same second time, or just launch Camera app itself and accept/deny permissions request from it, all is fixed. Camera takes exactly one shot and allows me to accept it, after that it comes back to my app and processes received photo.
So while for the user experience this behaviour can be just annoying, it with a high probability makes the Firebase robotests to fail on this point.
What could be the possible root of a problem?

Why is data being shown when screen rotates in jetpack compose

I'm facing this issue where the data I'm retrieving from an API, https://randomuser.me/api/ at first compose it doesn't load.
But every time I rotate the screen the data updates.
First load
After screen rotation
View
class MainActivity : ComponentActivity() {
private val userViewModel : UserViewModel by viewModels()
private var userList: List<UserModel> = listOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel.userModel.observe(this, Observer {
userList = it
})
userViewModel.onCreate()
setContent {
ListUsers(userList = userList)
}
}
}
ViewModel
class UserViewModel : ViewModel() {
val userModel = MutableLiveData<List<UserModel>>()
var getRandomUsersUseCase = RandomUsersUseCase()
fun onCreate() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
}
Use State to ensure the data changes trigger recomposition of the Composable.
If you use another observable type such as LiveData in Compose, you
should convert it to State before reading it in a composable using
a composable extension function like LiveData.observeAsState().
Changes to your code would be,
val userListState by userViewModel.userModel.observeAsState()
setContent {
ListUsers(userList = userListState)
}
Why does it shows the data during rotation?
When rotating the screen or during any other configuration changes, the activity will be recreated.
More info on that here - Docs
In most cases, you would not require data to be changed when the screen rotates.
If you want to persist the data even after screen rotation, move the code inside onCreate() in your UserViewModel to the init block, like this.
init {
getData()
}
fun getData() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
If you need to refresh the data on any other event like button click, swipe to refresh, etc, just call the getData() again on the event handler.
P.S: Check correct imports are added as required.
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue

Kotlin: Is it possible to make a function, which calls a retrofit service, to return a String value?

I have a Fragment and a View Model.
The layout of the Fragment contains a button.
When the button is clicked, we try to get an API response, which contains a url.
That url is used to start an intent to open a web page.
I am currently accomplishing this with event driven programming.
The button in the Fragment is clicked.
The function in the view model is called to get the API response, which contains the url.
The url in the view model is assigned as live data, which is observed in the fragment.
The fragment observes the url live data has changed. It attempts to launch the WebView with the new url.
Can the Fragment skip Observing for the url and directly get the ViewModel function to return a string?
Here is the code for the Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Set the OnClickListener
myButton.setOnClickListener {
myViewModel.getUrlQueryResults()
}
// Observables to open WebView from Url
myViewModel.myUrl.observe(viewLifecycleOwner, Observer {
it?.let{
if (it.isEmpty()) {
// No Url found in this API response
}
else {
// Open the WebView
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it))
startActivity(intent)
}
catch (e: Exception) {
// Log the catch statement
}
}
}
})
}
Here is the code for the ViewModel:
// Live data observed in fragment. When this changes, fragment will attempt to launch Website with the url
private val _myUrl = MutableLiveData<String>()
val myUrl: LiveData<String>
get() = _myUrl
// Possible to make this return a string?
fun getUrlQueryResults() {
InfoQueryApi.retrofitService.getInfo(apiKey).enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
try {
// Store the response here
apiResponse = parseInfoJsonResult(JSONObject(response.body()!!))
// Grab the url from the response
var urlFromResponse = apiResponse?.url
if (urlFromResponse.isNullOrEmpty()) {
urlFromResponse = ""
}
// Store the urlFromResponse in the live data so Fragment can Observe and act when the value changes
_myUrl.value = urlFromResponse
} catch (e: Exception) {
// Log catch statement
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
// Log error
}
})
}

Refresh fragment from ViewPager when specifc tab is selected

I would like to refresh fragment(SentAtt) from ViewPager. I use TabLayout to detect selected fragment but i don't know how can i refresh specific fragment when tab is selected.
tabLayout!!.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
viewPager.currentItem = tab.position
if(tab.position==1){
val sentAtt:SentAtt
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
you can get fragment by tag use this code
tabLayout!!.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
viewPager.currentItem = tab.position
if(tab.position==1){
val fragment =supportFragmentManager.findFragmentByTag("android:switcher:" + R.id.pager +
":"
+
viewPager.currentItem ) as? SentAtt
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
To correctly encapsulate your Fragment you should not try to refresh it from outside the Fragment.
Viewpager2 and Viewpager with BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT flag set then a Fragment will only be moved to the RESUMED state when it is selected, thus you should put you refresh code in the Fragments onResume method.
As the refresh code is inside the Fragment you don't work out which fragment to refresh.