Why is data being shown when screen rotates in jetpack compose - kotlin

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

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.

Which one should I choose between collecting Flow from ViewModel or collecting Flow in Compose directly

The code A collect Flow in ViewModel, and convert a Flow as hot Flow using MutableStateFlow, then use collectAsState() in Compose.
The code B collect Flow in Compose, and use collectAsState in Compose directly.
Which one is the better between code A and code B ?
In my mind, the code A convert a Flow as hot Flow in ViewModel, it will retain memeory and waster resources, is it a good way?
Code A
#Composable
fun BookListScreen(
viewModel: BookListViewModel,
...
) {
val booksListUiState by viewModel.uiState.collectAsState()
...
}
#HiltViewModel
class BookListViewModel #Inject constructor(
private val bookUseCase: BookUseCase
) : ViewModel() {
private var loadBooksJob: Job? = null
private val _uiState = MutableStateFlow(BookListUiState())
val uiState = _uiState.asStateFlow()
init {
loadBooks()
}
fun loadBooks() {
loadBooksJob?.cancel()
loadBooksJob = viewModelScope.launch {
bookUseCase.listBooks().collect { resultState ->
_uiState.update {
it.copy(bookListState = resultState)
}
}
}
}
...
}
override fun loadBooks(): Flow<ResultState<List<Book>>> {
...
}
Code B
#Composable
fun Greeting(
name: String,
mViewMode:SoundViewModel= viewModel()
) {
Column(
) {
val myResult by mViewMode.listRecord().collectAsState(initial =Result.Error(Exception()) )
..
}
#HiltViewModel
class SoundViewModel #Inject constructor(
private val aSoundMeter: RecordRepository
): ViewModel()
{
fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
#Dao
interface RecordDao {
#Query("SELECT * FROM record_table ORDER BY createdDate desc")
fun listRecord(): Flow<List<RecordEntity>>
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao): IRecordRepository {
override fun listRecord(): Flow<Result<List<MRecord>>> {
...
}
}
In Code A, you're keeping a flow into the view model. Therefore, this flow will be saved in case of the user rotates the device. Also, the loadBooks will not be called in case of a recomposition in BookListScreen.
In Code B, listRecord is called every time a recomposition happens, therefore every time the screen is redrawn you're doing a data base call. Since your DB is local, the damage does not look so bad, but if you're using a remote DB (like Firebase or REST web service) this code will perform a network call in every recomposition, including when you rotate the device or returning to this screen.
Check it out this Playlist from Android Developers Channel. In particular this video (which is more focused in your problem).

What is the correct way to use viewLifecycleOwner in MainActivity?

I am trying to figure out how to correctly use the viewLifecycleOwner in MainActivity, I have read and been told lifecycles are used with fragments. However, I am not implementing fragments in my app. When adding observers in the code, I am using "this" in place of viewLifecycleOwner. This would not rise any errors, but will eventually not work as it doesn't bind data properly in the virtual device (when running the app, it only displays a blank page for the app without data or images). So far, what I have in MainActivity is the following code.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: DrinkViewModel
// Contains all the views
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// Use Data Binding to get reference to the views
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.drinkButton.setOnClickListener {
onDrinkClicked()
}
viewModel.revenue.observe(this, Observer { newRevenue ->
binding.revenueText.text = newRevenue.toString()
})
viewModel.drinksSold.observe(this, Observer { newAmount ->
binding.amountSoldText.text = newAmount.toString()
})
}
}
After EpicPandaForce's comment, I focused on whether I was correctly binding the data and images. I realized I was not. I was mistakenly binding revenue and amountSold as texts. I was also trying to set newRevenue and newAmount to strings. Revenue and amountSold were supposed to be passed as Integers. The following code is the correct one.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: DrinkViewModel
// Contains all the views
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// Use Data Binding to get reference to the views
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.drinkButton.setOnClickListener {
onDrinkClicked()
}
viewModel.revenue.observe(this, Observer { newRevenue ->
binding.revenue = newRevenue
})
viewModel.drinksSold.observe(this, Observer { newAmount ->
binding.drinkSold = newAmount
})
}
}

Avoid fragment recreation when opening from notification navigation component

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)
}
}

how to implement MVVM

I want to use MVVM architecture, I have a view pager which has two fragments. each fragment has a RecyclerView. I created a view model class that extends AndroidViewModel because I needed context to pass to my repository which gets data from an API class that gets data from the server using volley. I expected when a rotate my phone, my ViewModel class doesn't be recalled but based on the logs which I set, it seems when I rotate the phone everything will be starting again.
is it ok? or I have something wrong?
this is my Fragment codes:
val viewModel: Fragment1ViewModel =
ViewModelProviders.of(this).get(Fragment1ViewModel(Application())::class.java)
viewModel.getData().observe(this, Observer {
Log.i("Log","data is Loaded in activity")
})
this is ViewModel Class codes:
class Fragment1ViewModel(application: Application) : AndroidViewModel(application) {
private val repository=Repository(application)
fun getData():LiveData<String>{
Log.i("Log","get data in view model")
return repository.getListItem()
}
}
this is my repository codes:
class Repository(private val context: Context) {
companion object {
private lateinit var serverFetch: ServerFetch
}
private fun setContext() {
serverFetch = ServerFetch(context)
}
fun getListItem():LiveData<String>{
setContext()
return serverFetch.getData()
}
}
and this is my ServerFetch class which works as an API for repository:
class ServerFetch(private val context: Context) {
private val api: String = "https://jsonplaceholder.typicode.com/posts"
private val result = MutableLiveData<String>()
private fun connect() {
val request = StringRequest(Request.Method.GET, api, Response.Listener {
Log.i("Log", "ServerFetch Successfully")
result.value = it
}, Response.ErrorListener {
Log.i("Log", "ServerFetch field: ${it.toString()}")
})
val queue = Volley.newRequestQueue(context)
queue.add(request)
}
fun getData(): LiveData<String> {
connect()
return result
}
}
and here is my Logs when app will run for the first time:
I/Log: get data in view model
I/Log: ServerFetch Successfully
I/Log: data is Loaded in activity
and this is my logs when I rotate my phone
I/Log: get data in view model
I/Log: ServerFetch Successfully
I/Log: data is Loaded in activity
You should fetch it at the instantiation of the view model.
ViewModel:
class Fragment1ViewModel(application: Application) : AndroidViewModel(application) {
private val repository = Repository(application)
val data = repository.getListItem()
}
Fragment:
viewModel.data.observe(this, Observer {
Log.i("Log","data is Loaded in activity")
})