user1795832
user1795832

Reputation: 2160

Android app retrieve data from server, save in database and display to user

I'm rewriting an app that involves retrieving data from a server via REST, saving that to the database on each Android device, and then displaying that data to the user. The data being retrieved from the server has a "since" parameter, so it won't return all data, just data that has changed since the last retrieval.

I have the retrieval from the server working fine, but I'm not sure the best way to save that data to the database, then show it to the user. I'm using Kotlin, Retrofit, Room and LiveData.

The code below is a simplified version of what I'm actually doing, but it gets the point across.

MyData.kt (model)

@Entity(tableName = "MyTable")
data class MyData(
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "id")
  var id Int? = null,

  @SerializedName("message")
  @ColumnInfo(name = "message")
  var message: String? = null
) {
    companion object {
      fun fromContentValues(values: ContentValues): MyData {
        val data = MyData()
        // Do this for id and message
        if (values.containsKey("id") {
          data.id = values.getAsInteger("id")
        }
      }
    }
}

DataViewModel.kt

class DataViewModel(application: Application) : AndroidViewModel(application) {
  private val repository = DataRepository()

  fun data(since: Long) =
    liveData(Dispatchers.IO) {
      val data = repository.getDataFromServer(since)
      emit(data)
    }

  fun saveData(data: List<MyData>) =
    liveData(Dispatchers.Default) {
      val result = repository.saveDataToDatabase(data)
      emit(result)
    }

  fun data() =
    liveData(Dispatchers.IO) {
      val data = repository.getDataFromDatabase()
      emit(data)
    }
}

DataRepository.kt

class DataRepository(application: Application) {

  // I won't add how the Retrofit client is created, it's standard
  private var client = "MyUrlToGetDataFrom"

  private var myDao: MyDao

  init {
    val myDatabase = MyDatabase.getDatabase(application)
    myDao = myDatabase!!.myDao()
  }

  suspend fun getDataFromServer(since: Long): List<MyData> {
    try {
        return client.getData(since)
    } catch (e: Exception) {

    }
  }

  fun getDataFromDatabase(): List<MyData> = myDao.getAll()

  suspend fun insertData(data: List<MyData>) = 
    myDao.insertData(data)
  }

MyDao.kt

@Dao
interface PostsDao {

    @Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
    suspend fun getAllData(): List<MyData>

    @Insert
    suspend fun insertData(data: List<MyData>)
}

ListActivity.kt

private lateinit var mDataViewModel: DataViewModel

override fun onCreate(savedInstanceBundle: Bundle?) {
  super.onCreate(savedInstanceBundle)
  mDataViewModel = ViewModelProvider(this, DataViewModelFactory(contentResolver)).get(DataViewModel::class.java)

  getData()
}

private fun getData() {
  mDataViewModel.data(getSince()).observe(this, Observer {
    saveData(it)
  })
}

private fun saveData(data: List<MyData>) {
  mDataViewModel.saveData(data)
  mDataViewModel.data().observe(this, Observer {
    setupRecyclerView(it)
  })
}

ListActivity.kt, and possibly the ViewModel and Repository classes where it uses coroutines, are where I'm stuck. getData() retrieves the data from the server without a problem, but when it comes to saving it in the database, then taking that saved data from the database and displaying it to the user I'm unsure of the approach. As I mentioned I'm using Room, but Room will not let you access the database on the main thread.

Remember, I have to save in the database first, then retrieve from the database, so I don't want to call mDataViewModel.data().observe until after it saves to the database.

What is the proper approach to this? I've tried doing CoroutineScope on the mDataViewModel.saveData() then .invokeOnCompletion to do mDataViewModel.data().observe, but it doesn't save to the database. I'm guessing I'm doing my Coroutines incorrectly, but not sure where exactly.

It will also eventually need to delete and update records from the database.

Upvotes: 1

Views: 6496

Answers (1)

Roaim
Roaim

Reputation: 2358

Updated Answer

After reading comments and updated question I figured out that you want to fetch a small list of data and store it to database and show all the data stored in the database. If this is what you want, you can perform the following (omitted DataSouce for brevity) -

In PostDao You can return a LiveData<List<MyData>> instead of List<MyData> and observe that LiveData in the Activity to update the RecyclerView. Just make sure you remove the suspend keyword as room will take care of threading when it returns LiveData.

@Dao
interface PostsDao {
    @Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
    fun getAllData(): LiveData<List<MyData>>

    @Insert
    suspend fun insertData(data: List<MyData>)
}

In Repository make 2 functions one for fetching remote data and storing it to the database and the other just returns the LiveData returned by the room. You don't need to make a request to room when you insert the remote data, room will automatically update you as you are observing a LiveData from room.

class DataRepository(private val dao: PostsDao, private val dto: PostDto) {

    fun getDataFromDatabase() = dao.getAllData()

    suspend fun getDataFromServer(since: Long) = withContext(Dispatchers.IO) {
        val data = dto.getRemoteData(since)
        saveDataToDatabase(data)
    }

    private suspend fun saveDataToDatabase(data: List<MyData>) = dao.insertData(data)
}

Your ViewModel should look like,

class DataViewModel(private val repository : DataRepository) : ViewModel() {

    val dataList = repository.getDataFromDatabase()

    fun data(since: Long) = viewModelScope.launch {
       repository.getDataFromServer(since)
    }
}

In the Activity make sure you use ListAdapter

private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter

override fun onCreate(savedInstanceBundle: Bundle?) {
    ...
    mDataViewModel.data(getSince())
    mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}

Initial Answer

First of all, I would recommend you to look into Android Architecture Blueprints v2. According to Android Architecture Blueprints v2 following improvements can be made,

  1. DataRepository should be injected rather than instantiating internally according to the Dependency Inversion principle.
  2. You should decouple the functions in the ViewModel. Instead of returning the LiveData, the data() function can update an encapsulated LiveData. For example,

    class DataViewModel(private val repository = DataRepository) : ViewModel() {
        private val _dataList = MutableLiveData<List<MyData>>()
        val dataList : LiveData<List<MyData>> = _dataList
    
        fun data(since: Long) = viewModelScope.launch {
            val list = repository.getData(since)
            _dataList.value = list
        }
        ...
    }
    
  3. Repository should be responsible for fetching data from remote data source and save it to local data source. You should have two data source i.e. RemoteDataSource and LocalDataSource that should be injected in the Repository. You can also have an abstract DataSource. Let's see how can you improve your repository,

    interface DataSource {
        suspend fun getData(since: Long) : List<MyData>
        suspend fun saveData(list List<MyData>)
        suspend fun delete()
    }
    
    class RemoteDataSource(dto: PostsDto) : DataSource { ... }
    class LocalDataSource(dao: PostsDao) : DataSource { ... }
    
    class DataRepository(private val remoteSource: DataSource, private val localSource: DataSource) {
        suspend fun getData(since: Long) : List<MyData> = withContext(Dispatchers.IO) {
            val data = remoteSource.getData(since)
            localSource.delete()
            localSource.save(data)
            return@withContext localSource.getData(since)
        }
        ...
    }
    
  4. In your Activity, you just need to observe the dataList: LiveData and submit it's value to ListAdapter.

    private lateinit var mDataViewModel: DataViewModel
    private lateinit var mAdapter: ListAdapter
    
    override fun onCreate(savedInstanceBundle: Bundle?) {
        ...
        mDataViewModel.data(since)
        mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
    }
    

Upvotes: 2

Related Questions