Best practice to implement HTTP API service with Jetpack Compose + Retrofit - android

Scenario:
This app allows the user to scan barcodes in the supermarket, and add the items to their virtual cart. The virtual cart should show an image, name and price. This data is sourced from an HTTP API.
Prerequisites:
I have a simple Product object, which also tracks the quantity:
class Product(
val barcode: String,
val name: String,
val price: Double,
val category: String,
val image: Image?,
initialQuantity: Int = 1
) {
var quantity by mutableStateOf(initialQuantity)
}
Then, I have a viewModel of my cart, which contains a list of Products:
class CartViewModel: ViewModel() {
// List of my Products in the cart.
private val _cart: MutableList<Product> = mutableStateListOf<Product>()
// Getter for the cart
val cart: List<Product>
get() = _cart
/**
* Removes a product from the cart. Quantities are ignored, so the product is always removed completely.
*/
fun remove(product: Product) {
println("Removing product: ${product.name} from cart")
_cart.remove(product)
}
/**
* Adds a product to the cart. If the product is already in the cart, the quantity is increased.
*/
fun add(product: Product) {
val existingProduct = _cart.find { it.barcode == product.barcode }
if (existingProduct != null) {
existingProduct.quantity += product.quantity
println("Increased quantity of product: ${product.name} to ${existingProduct.quantity}")
} else {
_cart.add(product)
println("Added product: ${product.name} to cart")
}
}
}
Now, I want to connect this viewModel to an API.
Problem:
The user scans the barcode '1', now the item is added to the list. Now I'd like to get the image and price from the HTTP API. I read this guide from Android about data fetching, however, it does not seem to be compatible with the idea of a simple object in a list. What is the proper way to implement this?
I tried a different approach using a Service and Repository as described in the documentation, however, this does not work as all, the the CartViewModel needs the productRepository in the constructor, which does not work in Jetpack Compose?
interface ProductService {
#GET("/product?barcode={barcode}")
suspend fun getProduct(#Path("barcode") barcode: String): Product
}
class ProductRepository constructor(
private val productService: ProductService
) {
suspend fun getProduct(barcode: String): Product {
return productService.getProduct(barcode)
}
}
/**
* Our viewmodel for representing the cart
*/
class CartViewModel constructor(
productRepository: ProductRepository
): ViewModel() {
private val _cart: MutableList<Product> = mutableListOf<Product>()
val cart: List<Product>
get() = _cart
fun add(product: Product) {
_cart.add(product)
}
fun remove(product: Product) {
_cart.remove(product)
}
init {
viewModelScope.launch {
_cart.add(productRepository.getProduct("1"))
}
}
}

Related

Android MVVM + Room creating LiveData RecyclerViewItem objects by other LiveData objects

I have Room Entity Class "Symptom" with name of Symptom and id of it.
#Entity(tableName = "symptoms")
data class Symptom(
#PrimaryKey #NonNull val id: Int,
val name: String) {
override fun toString(): String {
return "Symptom $id: $name"
}
}
I'm getting it in the following classses:
SymptomDao
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms WHERE id=:id LIMIT 1")
fun getSymptom(id: Int): Symptom
#Query("SELECT * FROM symptoms")
fun getAllSymptoms(): LiveData<List<Symptom>>
}
SymptomRepository
class SymptomRepository(private val symptomDao: SymptomDao) {
fun getSymptom(id: Int) = symptomDao.getSymptom(id)
fun getAllSymptoms() = symptomDao.getAllSymptoms()
}
SymptomsViewModel
class SymptomsViewModel(symptomRepository: SymptomRepository): ViewModel() {
private val symptomsList = symptomRepository.getAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
fun getAllSymptoms(): LiveData<List<Symptom>> {
return symptomsList
}
fun getAllSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
I have RecyclerView with list of SymptomItem with Checkboxes to remember which Symptoms of a list users chooses:
data class SymptomItem(
val symptom: Symptom,
var checked: Boolean = false)
Question
My question is how can I get LiveData<List<SymptomItem>> by LiveData<List<Symptom>>? I have just started learning MVVM and I can't find a simply answer how to do that. I have already tried to fill this list in various ways, but It loses checked variable every time I rotate my phone. I'll be grateful for any hints.
You'll need to store which items are checked by storing their Ids in a List within the ViewModel. Then you'll have combine the list of your Symptom objects and the list of which items are checked, and generate the list of SymptomItem objects.
I'm going to use Kotlin Flow to achieve this.
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms")
fun flowAllSymptoms(): Flow<List<Symptom>>
}
class SymptomRepository(private val symptomDao: SymptomDao) {
fun flowAllSymptoms() = symptomDao.flowAllSymptoms()
}
class SymptomsViewModel(
private val symptomRepository: SymptomRepository
) : ViewModel() {
private val symptomsListFlow = symptomRepository.flowAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
private var checkedIdsFlow = MutableStateFlow(emptyList<Int>())
init {
viewModelScope.launch {
collectSymptomsItems()
}
}
private suspend fun collectSymptomsItems() =
flowSymptomsItems().collect { symptomsItems ->
symptomsItemsList.postValue(symptomsItems)
}
private fun flowSymptomsItems() =
symptomsListFlow
.combine(checkedIdsFlow) { list, checkedIds ->
list.map { SymptomItem(it, checkedIds.contains(it.id)) }
}
fun checkItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).add(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun uncheckItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).remove(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun getSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
In your Fragment, observe getSymptomsItems() and update your adapter data.
The code is not tested, you may have to make small adjustments to make it compile.

Search functionality in room database

I want to create a search function for my user to quick access to my items .
Well , the first thing is that i have my product in a room table(List) and store them in database and show them with a recyclerview in the my main activity(Home activity ) .
So i want code a Query to search between them after user click on button search .
I code my query and after use it in my home activity nothing happend .i'm using mvvm model. pls help me with this .
Code :
My Table (List of Product ) :
#Entity(tableName = "cart")
data class RoomTables(
#PrimaryKey(autoGenerate = true) val id: Int?,
#ColumnInfo val title: String,
#ColumnInfo val price: Int,
#ColumnInfo val image: Int,
#ColumnInfo var amount: Int
)
My dao :
#Query ("SELECT * FROM cart WHERE title LIKE :search")
fun searchItem (search : String?):List<RoomTables>
My Repository :
fun searchItem(search :String) = db.GetDao().searchItem(search)
My Viewmodel :
fun searchItem(search : String) = CoroutineScope(Dispatchers.Default).launch {
repository.searchItem(search)
}
And HomeActivity :
class HomeActivity : AppCompatActivity() {
lateinit var viewModelRoom: ViewModelRoom
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity)
val list = ArrayList<RoomTables>()
for (i in 0..20) {
list.add(
RoomTables(
null, "$i banana", 12,
R.drawable.bannana, 0
)
)
}
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(list, context)
}
val database = DataBaseRoom(this)
val repositoryCart = RepositoryCart (database)
val factoryRoom = FactoryRoom(repositoryCart)
viewModelRoom = ViewModelRoom(repositoryCart)
viewModelRoom = ViewModelProvider(this , factoryRoom ).get(ViewModelRoom::class.java)
val editText : EditText = findViewById(R.id.edittextSearch)
val searchbtn : ImageView = findViewById(R.id.search_main)
searchbtn.setOnClickListener{
viewModelRoom.searchItem(editText.text.toString())
}
Let's try this approach.
First get list items from the table.
#Query ("SELECT * FROM cart")
fun searchItem():List<RoomTables>
Now from your repository.
fun searchItem() : List<RoomTables> = db.GetDao().searchItem()
In ViewModel.
fun searchItem(search : String): <List<RoomTables> {
filterWithQuery(query)
return filteredList
}
private fun filterWithQuery(query: String, repository: YourRepository) {
val filterList = ArrayList<RoomTables>()
for (currentItem: RoomTables in repository.searchItem()) {
val formatTitle: String = currentItem.title.toLowerCase(Locale.getDefault())
if (formatTitle.contains(query)) {
filterList.add(currentItem)
}
}
filteredList.value = filterList
}
Make sure you add Coroutines above.
Now you have all elements filtered and returns new list items based on search query user entered.
In your fragment or activity observe data.
searchbtn.setOnClickListener{
viewModel.searchItem(query).observe(viewLifecycleOwner, Observer { items -> {
// Add data to your recyclerview
}
}
The flow and approach is correct and it is working, it is hard to follow your code since i'm not sure if the return types match because you are not using LiveData, in this case you must.
If you found confusing or hard to follow, i have a working example in github, compare and make changes.
https://github.com/RajashekarRaju/ProjectSubmission-GoogleDevelopers

How to use SearchView with LiveData and ViewModel in Room

i want to use SearchView for search some element in room db and i have a problem with this because i cant use getFilter in RecyclerViewAdapter because i have ViewModel maybe whoes know how to combine all of this element in one project.
I search one way use Transormations.switchMap. But I couldn’t connect them.
ProductViewModel
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository: ProductRepository
val allProducts: LiveData<List<ProductEntity>>
private val searchStringLiveData = MutableLiveData<String>()
init {
val productDao = ProductsDB.getDatabase(application, viewModelScope).productDao()
repository = ProductRepository(productDao)
allProducts = repository.allProducts
searchStringLiveData.value = ""
}
fun insert(productEntity: ProductEntity) = viewModelScope.launch {
repository.insert(productEntity)
}
val products = Transformations.switchMap(searchStringLiveData) { string ->
repository.getAllListByName(string)
}
fun searchNameChanged(name: String) {
searchStringLiveData.value = name
}
}
ProductDao
interface ProductDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertProduct(productEntity: ProductEntity)
#Query("SELECT * from products")
fun getListAllProducts(): LiveData<List<ProductEntity>>
#Query("DELETE FROM products")
suspend fun deleteAll()
#Query("SELECT * FROM products where product_name_ent LIKE :name or LOWER(product_name_ent) like LOWER(:name)")
fun getListAllByName(name: String):LiveData<List<String>>
}
ProductDao
#Query("SELECT * FROM products where product_name_ent LIKE :name or LOWER(product_name_ent) like LOWER(:name)")
fun getListAllByName(name: String):LiveData<List<ProductEntity>>
This Method in your dao should return LiveData<List<ProductEntity>> and not LiveData<List<String>>, because this query selects everything (*) from the entity and not a specific column.
it is similar to (#Query("SELECT * from products") fun getListAllProducts():LiveData<List<ProductEntity>>)
ProductViewModel
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository: ProductRepository
init {
val productDao = ProductsDB.getDatabase(
application,
viewModelScope
).productDao()
repository = ProductRepository(productDao)
}
private val searchStringLiveData = MutableLiveData<String>("") //we can add initial value directly in the constructor
val allProducts: LiveData<List<ProductEntity>>=Transformations.switchMap(searchStringLiveData)
{
string->
if (TextUtils.isEmpty(string)) {
repository.allProducts()
} else {
repository.allProductsByName(string)
}
}
fun insert(productEntity: ProductEntity) = viewModelScope.launch {
repository.insert(productEntity)
}
fun searchNameChanged(name: String) {
searchStringLiveData.value = name
}
}
Repository
...with the other method that you have, add the following:
fun allProducts():LiveData<List<ProductEntity>>=productDao.getListAllProducts()
fun allProductsByNames(name:String):LiveData<List<ProductEntity>>=productDao.getListAllByName(name)
In Your Activity Or Fragment Where you have the recyclerview adapter
inside onCreate() (if it is an activity)
viewModel.allProducts.observe(this,Observer{products->
//populate your recyclerview here
})
or
onActivityCreated(if it is a fragment)
viewModel.allProducts.observe(viewLifecycleOwner,Observer{products->
//populate your recyclerview here
})
Now set a listener to the searchView, everytime the user submit the query , call viewModel.searchNameChanged(// pass the new value)
Hope this helps
Regards,

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.
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,
DataRepository should be injected rather than instantiating internally according to the Dependency Inversion principle.
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
}
...
}
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)
}
...
}
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))
}

Room get id from execute method

I have a fragment with a RecyclerView and FloatingActionButton. Pressing the fab opens a dialog with the settings for the new item. By clicking on the positive button, I insert a new element into the database, doing the following steps:
1) Call from dialog
viewModel.createItem()
2) In the ViewModel
fun createItem() {
return repository.insertItem(Item("${name.value}"))
}
3) Repository looks like
#Singleton
class Repository #Inject constructor(
private val appExecutors: AppExecutors,
private val itemDao: ItemDao
) {
fun insertItem(item: Item) {
appExecutors.diskIO().execute{ itemDao.insert(item) }
}
fun loadItemList() = itemDao.getItemList()
}
4) Dao
#Dao
interface ItemDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(item: Item) : Long
#Query("SELECT * FROM item ORDER BY name")
fun getItemList() : LiveData<List<Item>>
}
5) Item
#Entity (tableName = "item")
data class Item(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "_id") val id: Long,
#ColumnInfo(name = "name") val name: String
) {
#Ignore
constructor(name: String) : this(0, name)
}
And then I want to navigate to the detail fragment for the inserted item. So i need the id of this item, which is returned by Room. But how can I return id from execute method in the repository? Or maybe you know another way to do this.
P.s. I use Dagger and all of the Architecture Components libraries.
itemDao.insert(item) already returns the id of the new element. What you need to do now is to send this id back to your frontend (or whereever you want to use the new id) via a callback.
You could create a listener:
interface ItemInsertedListener {
fun onItemInserted(itemId: Long)
}
Add the interface as optional argument for your insertItem method:
fun insertItem(item: Item, callback: ItemInsertedListener?) {
appExecutors.diskIO().execute {
val id = itemDao.insert(item)
callback?.onItemInserted(id)
}
}
And finally pass the ItemInsertedListener to your repo in your createItem method. When the callback returns you can redirect the user to the Fragment, given that the activity is still visible. Also bear in mind that the callback might run on a different thread, so explicitly switch to the UI thread before making UI changes.

Categories

Resources