I am a beginner in Android Development. So please help me to find out solution to my problem.
I am making Music app as practice project. As user search for an artist, API fetches results and results get stored in room database. I want to display results stored in room through ViewModel. But ViewModel is not showing results synchronously as data is inserted. At first search, it shows nothing. But after subsequent searches, it displays results.
Api call and insert data in Room.
artistResultsCallback.enqueue(object : Callback<ResponseModel>{
override fun onResponse(call: Call<ResponseModel>, response: Response<ResponseModel>) {
if(response.isSuccessful) {
hideProgressBar()
artistRepository.insertList(response.body()!!.getArtistModel()!!)
}
}
override fun onFailure(call: Call<ResponseModel>, t: Throwable) {
hideProgressBar()
Log.e("MainActivity","An error occurred")
}
})
my dao
#Dao
interface ArtistDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertArtists(artistList:List<Artist>)
#Query("SELECT * FROM Artists WHERE artistName LIKE :name")
fun getArtists(name:String):LiveData<List<Artist>>
#Query("DELETE FROM Artists")
fun deleteArtists()
}
my ViewModel
class ArtistViewModel(): ViewModel() {
fun getArtists(application: Application,name:String):LiveData<List<Artist>>{
var artistRepository =ArtistRepository(application)
var artistResults:LiveData<List<Artist>> = artistRepository.getResultsList(name)
return artistResults
}
}
my database
#Database(entities = [Artist::class],version = 3)
abstract class ArtistDatabase: RoomDatabase() {
abstract fun artistDao(): ArtistDao
companion object {
#Volatile
private var artistDb: ArtistDatabase? = null
fun getDatabaseInstance(context: Context): ArtistDatabase {
return artistDb ?: synchronized(this) {
artistDb ?: Room.databaseBuilder(context, ArtistDatabase::class.java, Constants.ARTIST_DATABASE).fallbackToDestructiveMigration().build()
}
}
}
}
My repository:
class ArtistRepository(application: Application) {
private var artistDao: ArtistDao?
init {
val artistDb= ArtistDatabase.getDatabaseInstance(application)
artistDao= artistDb.artistDao()
}
fun getResultsList(name:String):LiveData<List<Artist>>{
return artistDao!!.getArtists(name)
}
fun insertList(artist:List<Artist>){
CoroutineScope(Dispatchers.IO).launch{
artistDao!!.deleteArtists()
artistDao!!.insertArtists(artist)
}
}
}
Activity showing results
artistRecyclerView = findViewById(R.id.rv_artist)
var getIntent = intent.getStringExtra(Constants.ARTIST_NAME)
search_results_for.text="Search Results for: $getIntent"
artistViewModel = ViewModelProviders.of(this).get(ArtistViewModel::class.java)
var artistViewModelResults:LiveData<List<Artist>> = artistViewModel.getArtists(application,getIntent!!)
artistViewModelResults.observe(this, object:Observer<List<Artist>>{
override fun onChanged(t: List<Artist>) {
setUpUi(t)
}
})
Related
I use the following Code A to query records ,the data are wrapped with sealed class Result<out R>.
The val queryList is assigned with Result.Loading first, then it is assigned with Result.Success and wrapped data, the different UI will be loaded based the different value of queryList.
I think the queryList is only assigned with Result.Loading onetime, the queryList will keep return Result.Success when I launch mViewMode.listRecord() again and again, right?
So I hope the queryList is always assigned with Result.Loading before I launch mViewMode.listRecord() and return Result.Success , how can I fix the code?
Maybe do I need to modify Code B? or do I need to redesign data structure? or is there the better solution?
Code A
#Composable
fun Greeting() {
Column( ) {
val aResult: Result<Flow<List<MRecord>>> = Result.Loading
val queryList by produceState(initialValue = aResult) {
value = mViewMode.listRecord()
}
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
class SoundViewModel #Inject constructor(...): ViewModel()
{
fun listRecord(): Result<Flow<List<MRecord>>>{
return aRecordRepository.listRecord()
}
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
fun listRecord(): Result<Flow<List<MRecord>>> {
val temp = mRecordDao.listRecord()
return Result.Success(temp)
}
}
interface RecordDao {
#Query("SELECT * FROM record_table ORDER BY createdDate desc")
fun listRecord(): Flow<List<MRecord>>
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
Code B
...
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
fun listRecord(): Result<Flow<List<MRecord>>> {
val temp = mRecordDao.listRecord()
return Result.Success(temp) //How can I return Result.Loading first, then return Result.Success(temp)?
}
}
...
You can create a StateFlow in your view model representing Result and connect it to your RecordRepository as follows and then convert it to compose state using collectAsState
#Composable
fun Greeting(soundViewModel: SoundViewModel = SoundViewModel()) {
LaunchedEffect(Unit) {
soundViewModel.listRecord()
}
Column {
val queryList: Result by soundViewModel.dataResult.collectAsState()
when (queryList) {
is Result.Error -> {
...
}
is Result.Loading -> {
...
}
is Result.Success -> {
...
}
}
}
}
class SoundViewModel {
private val _dataResult: MutableStateFlow<Result> = MutableStateFlow(Result.Loading) // private mutable state flow
val dataResult = _dataResult.asStateFlow() // publicly exposed as read-only state flow
private val recordRepository = RecordRepository()
suspend fun listRecord() {
recordRepository.listRecord().collect {
_dataResult.value = Result.Success(it)
}
}
}
class RecordRepository {
fun listRecord(): Flow<List<Int>> = flow {
emit(listOf(1))
delay(1000L)
emit(listOf(2, 3))
}
}
sealed interface Result {
object Loading : Result
data class Success(val lst: List<Int>) : Result
data class Error(val err: Throwable) : Result
}
The tricky thing is: When you expose a Flow from Room, it only emits each list after there's a database change and a new query is completed. There is no in-between signal from the flow to indicate that the database change is detected but the new query isn't completed yet.
One possible solution is if you create a flow in your repository that when something that happens modifies the database, it restarts with a new emission of Result.Loading and then emits the DAO flow again. This way, your Flow is protected from missing any changes, even if you somehow miss showing a loading state.
You could use a shared flow in the Repository if there's more than one flow you want to handle this way. Use it with flatMapLatest, so every time you do something that is likely to cause a database change, the existing upstream listRecord flow from the DAO will be cancelled so you can get a new Loading state before collecting it again.
Disclaimer: I haven't tested this. It's only an idea.
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
private expectedChangeTicker = MutableSharedFlow<Unit>(replay = 1, bufferOverflow = BufferOverflow.DROP_OLDEST)
suspend fun addSomething(someThing: SomeThing) {
// Call this in every repository function that might cause listRecord to change
expectedChangeTicker.emit(Unit)
mRecordDao.addSomething(someThing)
}
val listRecord: Flow<Result<List<MRecord>> =
expectedChangeTicker.flatMapLatest {
flow {
emit(Result.Loading)
emitAll(mRecordDao.listRecord().map { Result.Success(it) })
}
}
}
I don't know Compose, so I'm not sure how you should expose this Flow in your ViewModel. Notice I changed it from Result<Flow...> to Flow<Result...>, which I think is more likely what you need. Here is my guess at how it should be done:
class SoundViewModel #Inject constructor(...): ViewModel()
{
val listRecord: Flow<Result<List<MRecord>>> =
aRecordRepository.listRecord
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
}
#Composable
fun Greeting() {
Column( ) {
val aResult: Result<List<MRecord>> = Result.Loading
val queryList by produceState(initialValue = Result.Loading) {
value = mViewMode.listRecord
}
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
I don't think you need produceState. You can simply collect the flow returned by Dao in your composable using collectAsState() extension function.
#Composable
fun Greeting() {
Column( ) {
val queryList by viewModel.listRecord.collectAsState(Result.Loading)
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
class SoundViewModel #Inject constructor(...): ViewModel() {
val listRecord = aRecordRepository.listRecord()
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao) {
fun listRecord(): Flow<List<MRecord>> {
return mRecordDao.listRecord()
}
}
Edit:
If you want to emit the loading state from the flow itself, you can do something like this:
class RecordRepository #Inject constructor(private val mRecordDao: RecordDao) {
fun listRecord(): Flow<Result<List<MRecord>>> {
return flow { // Create a new flow
emit(Result.Loading) // Emit loading state right away
mRecordDao.listRecord().collect {
emit(Result.Success(it)) // Emit success state upon receiving data from dao
}
}
}
}
I am using Room in my app with two entities. The whole implementation is below.
The Problem is, the given scheme is fixed, which means I do not change anything regarding DB. When I provide a new version of my app to Users over Google Play Console, I get the following issue in Cryshlytics although I did not change anything for DB, just edited UI or another things, which definetly nothing have to do with DB:
Fatal Exception: java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:154)
at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
.......
Now I am not sure if I change the version of DB, it would work. What is wrong here?
BTW the DB is called from a Fragment like this
val mainDb: MainRepository by lazy { MainRepository(requireContext()) }
val stateDb: StateRepository by lazy { StateRepository(requireContext()) }
What's wrong here?
AppDatabase:
#Database(entities = [Main::class, State::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract val mainDao: MainDao
abstract val stateDao: StateDao
companion object {
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase? =
INSTANCE ?: synchronized(AppDatabase::class) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
MY_DB
).allowMainThreadQueries()
.build()
return INSTANCE
}
}
}
Dao:
#Dao
interface StateDao {
#Query("SELECT * FROM $STATE")
fun getAll(): List<State>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(state: State)
#Update
fun update(state: State)
#Query("DELETE FROM $STATE")
fun drop()
}
#Dao
interface MainDao {
#Query("SELECT * FROM $MAIN")
fun getAll(): List<Main>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(main: Main)
#Update
fun update(main: Main)
#Query("DELETE FROM $MAIN")
fun drop()
}
Main:
#Entity(tableName = MAIN)
data class Main(
#PrimaryKey #ColumnInfo(name = NUMBER) val number: Int,
#ColumnInfo(name = CARD) val car: String? = EMPTY,
#ColumnInfo(name = MODEL) val model: String? = EMPTY
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(number)
parcel.writeString(car)
parcel.writeString(model)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Main> {
override fun createFromParcel(parcel: Parcel): Main {
return Main(parcel)
}
override fun newArray(size: Int): Array<Main?> {
return arrayOfNulls(size)
}
}
}
State:
#Entity(tableName = STATE)
data class State(
#PrimaryKey #ColumnInfo(name = NUMBER) val number: Int,
#ColumnInfo(name = STATE) val state: String? = EMPTY
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(number)
parcel.writeString(question)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<State> {
override fun createFromParcel(parcel: Parcel): State {
return State(parcel)
}
override fun newArray(size: Int): Array<State?> {
return arrayOfNulls(size)
}
}
}
Repository:
class MainRepository(context: Context) {
private val mainDao = AppDatabase.getInstance(context)?.mainDao
fun getAll(): List<Main>? {
return mainDao?.getAll()
}
fun insert(main: Main) {
AsyncInsert(mainDao).execute(main)
}
fun update(main: Main) {
mainDao?.update(main)
}
fun drop() {
mainDao?.drop()
}
private class AsyncInsert(private val dao: MainDao?) : AsyncTask<Main, Void, Void>() {
override fun doInBackground(vararg p0: Main?): Void? {
p0[0]?.let { dao?.insert(it) }
return null
}
}
}
class StateRepository(context: Context) {
private val stateDao = AppDatabase.getInstance(context)?.stateDao
fun drop() {
stateDao?.drop()
}
fun getAll(): List<State>? {
return stateDao?.getAll()
}
fun insert(state: State) {
AsyncInsert(stateDao).execute(state)
}
fun update(state: State) {
stateDao?.update(state)
}
private class AsyncInsert(private val dao: StateDao?) : AsyncTask<State, Void, Void>() {
override fun doInBackground(vararg p0: State?): Void? {
p0[0]?.let { dao?.insert(it) }
return null
}
}
}
Now I am not sure if I change the version of DB, it would work. What is wrong here?
Changing the version would probably not work as the schema, as far as Room is concerned, has changed.
There is either a bug or the schema has been changed.
However, changing the version, would, with a Migration that does nothing (so as to not get a "no migration specified" error), then fail but importantly with an expected (what Room expects the schema to be according to the Entities) found (the schema that exists) discrepancy. This, if there is no bug, could then be used to ascertain what has been changed.
My app using room as a database and retrofit as a network calling api.
i am observing database only as a single source of truth. every thing is working fine. But i am not finding solution of one scenario.
Like for the first time when user open app it do following operations
fetch data from db
fetch data from server
because currently database is empty so it sends empty result to observer which hide progress bar . i want to discard that event and send result to observer when server dump data to database. even server result is empty. so progress bar should always hide once their is confirmation no data exists.
in other words application should always rely on database but if it empty then it should wait until server response and then notify observer.
this is my code
observer
viewModel.characters.observe(viewLifecycleOwner, Observer {
Log.e("status is ", "${it.message} at ${System.currentTimeMillis()}")
when (it.status) {
Resource.Status.SUCCESS -> {
binding.progressBar.visibility = View.GONE
if (!it.data.isNullOrEmpty()) adapter.setItems(ArrayList(it.data))
}
Resource.Status.ERROR -> {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show()
binding.progressBar.visibility = View.GONE
}
Resource.Status.LOADING ->
binding.progressBar.visibility = View.VISIBLE
}
})
ViewModel
#HiltViewModel
class CharactersViewModel #Inject constructor(
private val repository: CharacterRepository
) : ViewModel() {
val characters = repository.getCharacters()
}
Repository
class CharacterRepository #Inject constructor(
private val remoteDataSource: CharacterRemoteDataSource,
private val localDataSource: CharacterDao
) {
fun getCharacters() : LiveData<Resource<List<Character>>> {
return performGetOperation(
databaseQuery = { localDataSource.getAllCharacters() },
networkCall = { remoteDataSource.getCharacters() },
saveCallResult = { localDataSource.insertAll(it.results) }
)
}
}
Utility function for all api and database handling
fun <T, A> performGetOperation(databaseQuery: () -> LiveData<T>,
countQuery: () -> Int,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery().map { Resource.success(it,"database") }.distinctUntilChanged()
emitSource(source)
val responseStatus = networkCall()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
}
}
LocalDataSource
#Dao
interface CharacterDao {
#Query("SELECT * FROM characters")
fun getAllCharacters() : LiveData<List<Character>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(characters: List<Character>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(character: Character)
}
DataSource
class CharacterRemoteDataSource #Inject constructor(
private val characterService: CharacterService
): BaseDataSource() {
suspend fun getCharacters() = getResult { characterService.getAllCharacters() }}
}
Base Data Source
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<T>): Resource<T> {
try {
Log.e("status is", "started")
val response = call()
if (response.isSuccessful) {
val body = response.body()
if (body != null) return Resource.success(body,"server")
}
return error(" ${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): Resource<T> {
Timber.d(message)
return Resource.error("Network call has failed for a following reason: $message")
}
}
Character Service
interface CharacterService {
#GET("character")
suspend fun getAllCharacters() : Response<CharacterList>
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T,message : String): Resource<T> {
return Resource(Status.SUCCESS, data, message)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, "loading")
}
}
}
CharacterList
data class CharacterList(
val info: Info,
val results: List<Character>
)
What is the best way by that i ignore database if it is empty and wait for server response and then notify observer
In the application, I am fetching data from the web and from the observer change method, Insert that data to local db. that's fine. but after inserted to db, My second observer not called so my UI will not update.
ManActivity.class
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var adapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout.activity_main)
setupViewModel()
setupUI()
setupObservers()
setupObservers2()
}
private fun setupViewModel() {
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(ApiHelper(RetrofitBuilder.apiService))
).get(MainViewModel::class.java)
}
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MainAdapter(arrayListOf())
recyclerView.addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
recyclerView.adapter = adapter
}
private fun setupObservers() {
viewModel.getUsers().observe(this, Observer {
//viewModel.getUserFromWeb()
it?.let { resource ->
when (resource.status) {
SUCCESS -> {
Log.d("MYLOG","MyAPIChange success")
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
resource.data?.let {
users -> viewModel.setUserListToDB(this,users)
//sleep(1000)
}
}
ERROR -> {
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
Log.d("MYLOG","MyAPIChange error")
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
LOADING -> {
Log.d("MYLOG","MyAPIChange loading")
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
})
}
private fun setupObservers2() {
viewModel.getUserFromDB(this).observe(this, Observer {
users -> retrieveList(users)
Log.d("MYLOG","..MyDBChange")
})
}
private fun retrieveList(users: List<User>) {
adapter.apply {
addUsers(users)
notifyDataSetChanged()
}
}
}
MyViewModel.class
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
//lateinit var tempUser : MutableLiveData<List<User>>
fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
//emit(mainRepository.getUsers()) //direct call
}
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
fun setUserListToDB(context: Context, userList: List<User>) {
/*GlobalScope.launch {
mainRepository.setUserList(context, userList)
}*/
CoroutineScope(Dispatchers.IO).launch {
mainRepository.setUserList(context, userList)
}
}
}
MyRepository.class
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers() // get from web
companion object {
var myDatabase: MyDatabase? = null
lateinit var userList: List<User>
fun initializeDB(context: Context): MyDatabase {
return MyDatabase.getDataseClient(context)
}
/*fun insertData(context: Context, username: String, password: String) {
myDatabase = initializeDB(context)
CoroutineScope(Dispatchers.IO).launch {
val loginDetails = User(username, password)
myDatabase!!.myDao().InsertData(loginDetails)
}
}*/
}
//fun getUserList(context: Context, username: String) : LiveData<LoginTableModel>? {
suspend fun getUserList(context: Context) : List<User> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.size.toString())
return userList
}
fun setUserList(context: Context,userList: List<User>){
myDatabase = initializeDB(context)
/*CoroutineScope(Dispatchers.IO).launch {
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
}*/
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
/*val thread = Thread {
myDatabase!!.myDao().InsertAllUser(userList)
}
Log.d("MYLOG","MyDBInserted")
thread.start()*/
}
}
DAO class
#Dao
interface DAOAccess {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun InsertAllUser(userList: List<User>)
// #Query("SELECT * FROM User WHERE Username =:username")
// fun getLoginDetails(username: String?) : LiveData<LoginTableModel>
#Query("SELECT * FROM User")
suspend fun getUserList() : List<User>
}
RetrofitBuilder
object RetrofitBuilder {
private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Please can you know that what I am doing wrong here and why the second observer was not called after insert to db
Actually It was called when the screen launched but that time data not inserted so list size was 0 and after insert data this method will not call again. But Once I close app and again start then data will display bcoz at launch time, this method call and data got
I don't have enough reputation to commet, therefore I just bring a suggestion in this answer:
Suggestion/Solution
Room supports LiveData out of the box. So in your DAO you can change
suspend fun getUserList() : List<User>
to
suspend fun getUserList() : LiveData<List<User>>
Then in your repository adjust to
suspend fun getUserList(context: Context) : LiveData<List<User>> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.value.size.toString())
return userList
}
and in the ViewModel
fun getUserFromDB(context: Context) = mainRepository.getUserList(context))
With these adjustments I think it should work.
Explaination
You used the liveData couroutines builder here
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
As far as I understand this builder, it is meant to execute some asynchronous/suspend task and as soon as this task finishes the liveData you created will emit the result. That means that you only once receive the state of the user list an emidiately emit the list to the observer one single time and then this liveData is done. It does not observe changes to the list in the DB the whole time.
That is why it works perfectly for observing the API call (you want to wait until the call is finished and emit the response one single time), but not for observing the DB state(you want to observe the user list in the DB all the time and emit changes to the observer whenever the list is changed)
I am starting a service in a different process from an activity.
The service is designed to run even when the app is closed.
After starting the service from the activity, I close the app. Now when I reopen the app the service may or may not be running. But I haven't find way to know if the service is running or not.
How can I achieve that?
FYI: I have checked all the related answers here on SO but none of them works when the service is running in a different process.
This is the closest answer I have got link. But this answer seems flawed, I would also like to hear your opinion on it too.
Here's what I am currently doing:
AndroidManifest.xml
<service
android:name=".services.MyService"
android:enabled="true"
android:exported="false"
android:process=":backgroundProcess" />
MainApplication.kt (purpose: to have only one instance of the SettingsRepository class)
class MainApplication : Application() {
val settingsRepository by lazy { SettingsRepository(this) }
}
SettingsRepository.kt (purpose: to save the running state of the service in Preference DataStore)
class SettingsRepository(context: Context) {
private val dataStore = context.createDataStore(name = "settings_prefs")
companion object {
val SERVICE_STATE_KEY = booleanPreferencesKey("SERVICE_STATE_KEY")
}
suspend fun saveServiceStateToDataStore(state: Boolean) {
dataStore.edit {
it[SERVICE_STATE_KEY] = state
}
}
val getServiceStateFromDataStore: Flow<Boolean> = dataStore.data.map {
val state = it[SERVICE_STATE_KEY] ?: false
state
}
}
Service.kt
private lateinit var settingsRepository: SettingsRepository
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
settingsRepository = (application.applicationContext as MainApplication).settingsRepository
saveStateToDataStore(true)
return START_REDELIVER_INTENT
}
private fun saveStateToDataStore(state: Boolean): Job {
return CoroutineScope(Dispatchers.IO).launch {
settingsRepository.saveServiceStateToDataStore(state)
}
}
Activity.kt
private fun observeDataFromViewModel() {
mainViewModel.readServiceStateFromRepository.observe(this, {state ->
Toast.makeText(this, "Service state changed to $state", Toast.LENGTH_SHORT).show()
// should get the new data when service stores it in onStartCommand but doesn't get it
// maybe because the service doesn't stores the data for some reason I am not aware of.
})
}
private fun handleClickListener() {
btn_start_service.setOnClickListener {
startForegroundService(serviceIntent)
}
}
btn_stop_service.setOnClickListener {
mainViewModel.saveServiceState(false)
stopService(serviceIntent)
}
}
ViewModel.kt
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val settingsRepository = (application.applicationContext as MainApplication).settingsRepository
val readServiceStateFromRepository = settingsRepository.getServiceStateFromDataStore.asLiveData()
fun saveServiceState(state: Boolean): Job {
return viewModelScope.launch(Dispatchers.IO) {
settingsRepository.saveServiceStateToDataStore(state)
}
}
}
Use the Messenger class to communicate with server https://developer.android.com/reference/android/app/Service.html#remote-messenger-service-sample
Or use a BroadcastReceiver to get service state from another process
class MainActivity : AppCompatActivity() {
private var receiver: TmpReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_main)
val intent = Intent(this, TmpService::class.java)
receiver = TmpReceiver()
val filter = IntentFilter().apply {
addAction("SERVICE_START")
addAction("SERVICE_STOP")
}
registerReceiver(receiver, filter)
startService(intent)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver)
receiver = null
}
}
class TmpService : Service() {
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
sendBroadcast("SERVICE_START")
}
override fun onDestroy() {
sendBroadcast("SERVICE_STOP")
super.onDestroy()
}
private fun sendBroadcast(action: String) {
Intent().also { intent ->
intent.action = action
sendBroadcast(intent)
}
}
}
class TmpReceiver: BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
Log.d("TmpReceiver", "action=${p1?.action}")
}
}
You can register one more receiver into the service to ping it from the activity.
About the closest answer it works for a single process app only