This is how I am inserting data into database using Room Persistence Library:
Entity:
#Entity
class User {
#PrimaryKey(autoGenerate = true)
public int id;
//...
}
Data access object:
#Dao
public interface UserDao{
#Insert(onConflict = IGNORE)
void insertUser(User user);
//...
}
Is it possible to return the id of User once the insertion is completed in the above method itself without writing a separate select query?
Based on the documentation here (below the code snippet)
A method annotated with the #Insert annotation can return:
long for single insert operation
long[] or Long[] or List<Long> for multiple insert operations
void if you don't care about the inserted id(s)
#Insert function can return void, long, long[] or List<Long>. Please try this.
#Insert(onConflict = OnConflictStrategy.REPLACE)
long insert(User user);
// Insert multiple items
#Insert(onConflict = OnConflictStrategy.REPLACE)
long[] insert(User... user);
According to the documentation functions annoted with #Insert can return the rowId.
If the #Insert method receives only 1 parameter, it can return a long, which is the new rowId for the inserted item. If the parameter is an array or a collection, it should return long[] or List<Long> instead.
The problem I have with this is that it returns the rowId and not the id and I still haven't found out how to get the id using the rowId.
Edit: I now know how to get the id from the rowId. Here is the SQL command:
SELECT id FROM table_name WHERE rowid = :rowId
Get the row ID by the following sniplet. It uses callable on an ExecutorService with Future.
private UserDao userDao;
private ExecutorService executorService;
public long insertUploadStatus(User user) {
Callable<Long> insertCallable = () -> userDao.insert(user);
long rowId = 0;
Future<Long> future = executorService.submit(insertCallable);
try {
rowId = future.get();
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return rowId;
}
Ref: Java Executor Service Tutorial for more information on Callable.
The return value of the insertion for one record will be 1 if your statement successfully.
In case you want to insert list of objects, you can go with:
#Insert(onConflict = OnConflictStrategy.REPLACE)
public long[] addAll(List<Object> list);
And execute it with Rx2:
Observable.fromCallable(new Callable<Object>() {
#Override
public Object call() throws Exception {
return yourDao.addAll(list<Object>);
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Object>() {
#Override
public void accept(#NonNull Object o) throws Exception {
// the o will be Long[].size => numbers of inserted records.
}
});
In your Dao, the insert query returns Long i.e. the rowId inserted.
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipes: CookingRecipes): Long
In your Model(Repository) class : (MVVM)
fun addRecipesData(cookingRecipes: CookingRecipes): Single<Long>? {
return Single.fromCallable<Long> { recipesDao.insertManual(cookingRecipes) }
}
In your ModelView class: (MVVM) Handle LiveData with DisposableSingleObserver.
Working sourcer reference : https://github.com/SupriyaNaveen/CookingRecipes
After a lot of struggle, I managed to solve this. Here is my solution using MMVM architecture:
Student.kt
#Entity(tableName = "students")
data class Student(
#NotNull var name: String,
#NotNull var password: String,
var subject: String,
var email: String
) {
#PrimaryKey(autoGenerate = true)
var roll: Int = 0
}
StudentDao.kt
interface StudentDao {
#Insert
fun insertStudent(student: Student) : Long
}
StudentRepository.kt
class StudentRepository private constructor(private val studentDao: StudentDao)
{
fun getStudents() = studentDao.getStudents()
fun insertStudent(student: Student): Single<Long>? {
return Single.fromCallable(
Callable<Long> { studentDao.insertStudent(student) }
)
}
companion object {
// For Singleton instantiation
#Volatile private var instance: StudentRepository? = null
fun getInstance(studentDao: StudentDao) =
instance ?: synchronized(this) {
instance ?: StudentRepository(studentDao).also { instance = it }
}
}
}
StudentViewModel.kt
class StudentViewModel (application: Application) : AndroidViewModel(application) {
var status = MutableLiveData<Boolean?>()
private var repository: StudentRepository = StudentRepository.getInstance( AppDatabase.getInstance(application).studentDao())
private val disposable = CompositeDisposable()
fun insertStudent(student: Student) {
disposable.add(
repository.insertStudent(student)
?.subscribeOn(Schedulers.newThread())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribeWith(object : DisposableSingleObserver<Long>() {
override fun onSuccess(newReturnId: Long?) {
Log.d("ViewModel Insert", newReturnId.toString())
status.postValue(true)
}
override fun onError(e: Throwable?) {
status.postValue(false)
}
})
)
}
}
In the Fragment:
class RegistrationFragment : Fragment() {
private lateinit var dataBinding : FragmentRegistrationBinding
private val viewModel: StudentViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialiseStudent()
viewModel.status.observe(viewLifecycleOwner, Observer { status ->
status?.let {
if(it){
Toast.makeText(context , "Data Inserted Sucessfully" , Toast.LENGTH_LONG).show()
val action = RegistrationFragmentDirections.actionRegistrationFragmentToLoginFragment()
Navigation.findNavController(view).navigate(action)
} else
Toast.makeText(context , "Something went wrong" , Toast.LENGTH_LONG).show()
//Reset status value at first to prevent multitriggering
//and to be available to trigger action again
viewModel.status.value = null
//Display Toast or snackbar
}
})
}
fun initialiseStudent() {
var student = Student(name =dataBinding.edName.text.toString(),
password= dataBinding.edPassword.text.toString(),
subject = "",
email = dataBinding.edEmail.text.toString())
dataBinding.viewmodel = viewModel
dataBinding.student = student
}
}
I have used DataBinding.Here is my XML:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="student"
type="com.kgandroid.studentsubject.data.Student" />
<variable
name="listener"
type="com.kgandroid.studentsubject.view.RegistrationClickListener" />
<variable
name="viewmodel"
type="com.kgandroid.studentsubject.viewmodel.StudentViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:id="#+id/nestedScrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="com.kgandroid.studentsubject.view.RegistrationFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constarintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="true">
<TextView
android:id="#+id/tvRoll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="Roll : 1"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="#+id/edName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tvRoll" />
<TextView
android:id="#+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Name:"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="#+id/edName"
app:layout_constraintEnd_toStartOf="#+id/edName"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="#+id/tvEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="#+id/edEmail"
app:layout_constraintEnd_toStartOf="#+id/edEmail"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="#+id/edEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edName" />
<TextView
android:id="#+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="#+id/edPassword"
app:layout_constraintEnd_toStartOf="#+id/edPassword"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="#+id/edPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edEmail" />
<Button
android:id="#+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:background="#color/colorPrimary"
android:text="REGISTER"
android:onClick="#{() -> viewmodel.insertStudent(student)}"
android:textColor="#android:color/background_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edPassword" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</layout>
I have struggled a lot to accomplish this with asynctask as room insert and delete operation must be done in a separate thread. Finally able to do this with Single
type observable in RxJava.
Here is Gradle dependencies for rxjava:
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.0.3'
If the #Insert method receives a single parameter, it can return a long value, which is the new rowId for the inserted item.
enter link description here
#Insert
suspend fun insert(myEntity: MyEntity):Long
Related
So I am developing and android app in Kotlin with coroutines and no matter what change I make, I still keep getting the message:
I/Choreographer: Skipped 59 frames! The application may be doing too much work on its main thread.
How can I get rid of it. I mean like I am only displaying nine photos... Below is my code
Model:
data class Food (
val id: String,
val name: String,
val price: String,
#Json(name = "img_url") val imgSrcUrl: String,
val type: String,
val description: String,
val average_rating: String,
val number_of_raters: String,
val special_price: String
)
data class FoodCategory(
val id: String,
val title: String,
val foods: List<Food>
)
ViewModel:
enum class NetworkStatus {LOADING, DONE, FAILED}
enum class FontFamily (#FontRes val fontRes: Int) {
POPPINS_BOLD(R.font.poppins_bold),
POPPINS(R.font.poppins)
}
class FoodOverviewViewModel(private val foodRepository: FoodRepository): ViewModel() {
private lateinit var foodProducts: List<Food>
//This is the data that is gonna be exposed to the viewmodel
//It will be submitted to a ListAdapter
private val _foodCategory = MutableLiveData<List<FoodCategory>>()
val foodCategory: LiveData<List<FoodCategory>>
get() = _foodCategory
//Used to display a progress bar for network status
private val _status = MutableLiveData<NetworkStatus>()
val status: LiveData<NetworkStatus>
get() = _status
init {
getOverviewProducts()
}
private fun getOverviewProducts() {
viewModelScope.launch(Dispatchers.Default) {
_status.postValue(NetworkStatus.LOADING)
try {
getUpdatedFood()
Log.i("getOverviewProducts","I am running on tread: $coroutineContext")
_status.postValue(NetworkStatus.DONE)
}catch (e: Exception) {
_status.postValue(NetworkStatus.FAILED)
}
}
}
private suspend fun getUpdatedFood() {
//withContext(Dispatchers.Default) {
val limiter = 6 //Number of items I want to get from the server
val foodCategory = arrayListOf<FoodCategory>()
Log.i("getUpdatedFood","I am running on tread: $coroutineContext")
val getRecommended = foodRepository.getRecommendedFood(limiter.toString())
foodCategory += FoodCategory(id = 0.toString(), title = "Recommended for you", foods = getRecommended)
val getSpecials = foodRepository.getSpecials(limiter.toString())
foodCategory += FoodCategory(id = 1.toString(), title = "Specials", foods = getSpecials)
_foodCategory.postValue(foodCategory)
//}
}
}
Repository:
class FoodRepository {
suspend fun getRecommendedFood(limiter: String) = withContext(Dispatchers.IO) {
Log.i("Resp-getRecommended","I am running on tread: $coroutineContext")
return#withContext ProductApi.retrofitService.getRecommended(limiter)
}
suspend fun getSpecials(limiter: String) = withContext(Dispatchers.IO) {
Log.i("Resp-getSpecials","I am running on tread: $coroutineContext")
return#withContext ProductApi.retrofitService.getSpecials(limiter)
}
}
BindingAdapters:
//Load image using Glide (in Food item recycleview)
#BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView , imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("http").build()
Glide.with(imgView.context)
.load(imgUri)
.apply(
RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image))
.into(imgView)
}
}
//set raters count (in Food item recycleview)
#BindingAdapter("ratersCount")
fun bindText(txtView: TextView, number_of_raters: String?) {
number_of_raters?.let {
val ratersCount = "(${number_of_raters})"
txtView.text = ratersCount
}
}
//update the progressbar visibilty (in outer-parent recycleview)
#BindingAdapter("updateStatus")
fun ProgressBar.updateStatus(status: NetworkStatus?) {
visibility = when (status) {
NetworkStatus.LOADING -> View.VISIBLE
NetworkStatus.DONE -> View.GONE
else -> View.GONE
}
}
//Hide or view an imageview based in the network Status. When network Error, an error image
//will show (in outer-parent recycleview)
#BindingAdapter("setNoInternet")
fun ImageView.setNoInternet(status: NetworkStatus?) {
when(status) {
NetworkStatus.LOADING -> {
visibility = View.GONE
}
NetworkStatus.DONE -> {
visibility = View.GONE
}
NetworkStatus.FAILED -> {
visibility = View.VISIBLE
setImageResource(R.drawable.ic_connection_error)
}
}
}
//Submit the list of FoodCatergory item to the outer-parent recycleview
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<FoodCategory>?) {
(recyclerView.adapter as FoodCategoryAdapter).submitList(data)
}
//Submit list the the Food item recyclew view (child recycleView)
#BindingAdapter("setProducts")
fun RecyclerView.setProducts(foods: List<Food>?) {
if (foods != null) {
val foodAdapter = FoodItemAdapter()
foodAdapter.submitList(foods)
adapter = foodAdapter
}
}
I have a Recycleview of Food Item and a Recycleview Pool of FoodCategory. If I comment out
_foodCategory.postValue(foodCategory)
in ViewModel: getUpdatedFood() than I do not get the message. However, when I submit the list to the outer recycleview (The one the hold the viewpool), than I get this answer. Please help. I been stuck on it for a while tryna get rid of that message.
Thank you..
Updated
Below is the adapeters and its view holders
FoodItem layout
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="foodItem"
type="com.example.e_commerceapp.models.Food"/>
<variable
name="font"
type="com.example.e_commerceapp.products.overview.FontFamily"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/child_item_main_layout"
android:background="#drawable/search_background"
android:layout_marginTop="10dp"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:layout_width="150dp"
android:layout_height="250dp">
<ImageView
android:layout_width="120dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:id="#+id/burger_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_height="160dp"
/>
<!-- app:imageUrl="#{foodItem.imgSrcUrl}"-->
<TextView
android:layout_width="match_parent"
android:layout_height="34dp"
android:layout_marginStart="5dp"
android:id="#+id/burger_title"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginEnd="5dp"
android:singleLine="true"
android:textColor="#B4000000"
app:layout_constraintTop_toBottomOf="#id/burger_image"
android:text="#{foodItem.name}"
android:textSize="12sp"
android:fontFamily="#font/poppins"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="35dp"
app:layout_constraintTop_toBottomOf="#id/burger_title"
android:layout_marginEnd="5dp"
android:gravity="center"
android:id="#+id/burger_price"
android:layout_marginStart="5dp"
app:layout_constraintStart_toEndOf="#id/special_price"
android:textColor="#D0000000"/>
<!-- app:price="#{foodItem.price}"-->
<!-- app:specialPrice="#{foodItem.special_price}"-->
<!-- app:fontRes="#{foodItem.special ? font.POPPINS : font.POPPINS_BOLD}"-->
<TextView
android:layout_width="wrap_content"
android:layout_height="35dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/burger_title"
android:layout_marginEnd="5dp"
android:gravity="center_vertical"
android:layout_marginStart="5dp"
android:id="#+id/special_price"
android:textColor="#D0000000"
android:visibility="gone"
android:textStyle="normal"
android:fontFamily="#font/poppins_bold"/>
<!-- app:setSpecialPrice="#{foodItem.special_price}"-->
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
app:layout_constraintTop_toBottomOf="#id/burger_price"
android:src="#drawable/ic_baseline_star_24"
android:visibility="#{foodItem.hasRating ? View.GONE : View.VISIBLE}"
android:id="#+id/rating_star"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="5dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="15dp"
android:layout_marginStart="5dp"
android:gravity="center"
android:textSize="12sp"
android:visibility="#{foodItem.hasRating ? View.GONE : View.VISIBLE}"
android:id="#+id/rating_count"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#id/burger_price"
android:text="#{foodItem.average_rating}"
android:layout_marginBottom="10dp"
app:layout_constraintStart_toEndOf="#id/rating_star"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="15dp"
android:id="#+id/number_of_raters"
android:textSize="12sp"
android:visibility="#{foodItem.hasRating ? View.GONE : View.VISIBLE}"
android:layout_marginStart="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/rating_count"
app:ratersCount="#{foodItem.number_of_raters}"
android:layout_marginBottom="10dp"
app:layout_constraintTop_toBottomOf="#id/burger_price"/>
<ImageView android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
FoodItem Adapter
class FoodItemAdapter: ListAdapter<Food ,
FoodItemAdapter.ItemFoodViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup , viewType: Int): ItemFoodViewHolder {
return ItemFoodViewHolder(
FoodItemBinding.inflate(LayoutInflater.from(parent.context),
parent, false))
}
override fun onBindViewHolder(holder: ItemFoodViewHolder , position: Int) {
val currentFood = getItem(position)
holder.bind(currentFood)
}
class ItemFoodViewHolder(private var binding: FoodItemBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(food: Food) {
binding.foodItem = food
binding.executePendingBindings()
}
}
object DiffCallback: DiffUtil.ItemCallback<Food>() {
override fun areItemsTheSame(oldItem: Food , newItem: Food): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Food , newItem: Food): Boolean {
return oldItem.id == newItem.id
}
}
}
FoodCategory layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="foodCategory"
type="com.example.e_commerceapp.models.FoodCategory"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:background="#fff"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:id="#+id/category_title"
android:layout_marginTop="16dp"
android:text="#{foodCategory.title}"
android:textColor="#2B2A2A"
android:fontFamily="#font/poppins_bold"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/nestedRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:setProducts="#{foodCategory.foods}"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/category_title"
tools:itemCount="4"
tools:listitem="#layout/food_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
FoodCategory Adapter
class FoodCategoryAdapter: ListAdapter<FoodCategory,
FoodCategoryAdapter.CategoryFoodViewHolder>(Companion) {
private val viewPool = RecyclerView.RecycledViewPool()
override fun onCreateViewHolder(parent: ViewGroup , viewType: Int): CategoryFoodViewHolder {
return CategoryFoodViewHolder(FoodCategoryBinding.inflate(LayoutInflater.from(parent.context),
parent, false))
}
override fun onBindViewHolder(holder: CategoryFoodViewHolder , position: Int) {
val currentFoodCategory = getItem(position)
holder.bind(currentFoodCategory)
}
inner class CategoryFoodViewHolder(private var binding: FoodCategoryBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(currentFoodCategory: FoodCategory?) {
binding.foodCategory = currentFoodCategory
binding.nestedRecyclerView.setRecycledViewPool(viewPool)
binding.executePendingBindings()
}
}
companion object: DiffUtil.ItemCallback<FoodCategory>() {
override fun areItemsTheSame(oldItem: FoodCategory , newItem: FoodCategory): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: FoodCategory, newItem: FoodCategory): Boolean {
return oldItem.id == newItem.id
}
}
}
The parent recycleView
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".products.overview.FoodOverviewFragment">
<data>
<variable
name="foodOverview"
type="com.example.e_commerceapp.products.overview.FoodOverviewViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:background="#color/grey"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:id="#+id/relative_layout"
android:layout_height="105dp"
android:elevation="8dp"
android:layout_marginBottom="5dp"
android:background="#fff"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:layout_width="200dp"
android:layout_marginTop="10dp"
android:layout_height="35dp"
android:id="#+id/logo_and_name"
android:src="#drawable/compony_logo_and_name"
android:layout_alignParentStart="true"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:id="#+id/notifications"
android:src="#drawable/ic_baseline_notifications_24"
android:layout_alignParentEnd="true"
android:paddingEnd="20dp"
android:paddingStart="20dp"/>
<TextView
android:layout_width="match_parent"
android:id="#+id/search"
android:layout_marginTop="10dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="#drawable/search_background"
android:layout_below="#id/logo_and_name"
android:gravity="center_vertical"
android:layout_height="match_parent"
android:layout_marginBottom="10dp"
android:paddingEnd="10dp"
android:paddingStart="10dp"
android:text="#string/search_text"
tools:ignore="RtlSymmetry"
app:drawableEndCompat="#drawable/ic_baseline_search_24"/>
</RelativeLayout>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:updateStatus="#{foodOverview.status}"
app:layout_constraintTop_toBottomOf="#id/relative_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="#+id/progressbar"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/relative_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="#+id/noInternetImage"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:id="#+id/foodCategory"
android:clipToPadding="false"
tools:itemCount="4"
tools:listitem="#layout/food_category"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_height="0dp"
app:listData="#{foodOverview.foodCategory}"
app:layout_constraintTop_toBottomOf="#id/relative_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The skipping frames likely has nothing to do with the code you posted: it sounds like a misconfiguration of RecyclerViews / Adapters to me. You'll need to post that code for it be more clear though.
However, even though what you posted likely isn't the culprit you can still optimize the coroutines code you have:
class FoodOverviewViewModel(private val foodRepository: FoodRepository): ViewModel() {
private lateinit var foodProducts: List<Food>
private val _foodCategory = MutableLiveData<List<FoodCategory>>()
val foodCategory: LiveData<List<FoodCategory>>
get() = _foodCategory
private val _status = MutableLiveData<NetworkStatus>()
val status: LiveData<NetworkStatus>
get() = _status
init {
getOverviewProducts()
}
private fun getOverviewProducts() {
viewModelScope.launch { // <------- Don't apply a custom scope here
_status.value = NetworkStatus.LOADING // <--- Don't call "postValue" here
try {
val food = getUpdatedFood() // <------ This is already using a background dispatcher
_foodCategory.value = food // <------- Emit this value here
_status.value = NetworkStatus.DONE
} catch (e: Exception) {
_status.value = NetworkStatus.FAILED
}
}
}
private suspend fun getUpdatedFood(): List<FoodCategory> { // <---- Return a value here
val limiter = 6 //Number of items I want to get from the server
val foodCategory = arrayListOf<FoodCategory>()
Log.i("getUpdatedFood","I am running on tread: $coroutineContext")
val getRecommended = foodRepository.getRecommendedFood(limiter.toString())
foodCategory += FoodCategory(id = 0.toString(), title = "Recommended for you", foods = getRecommended)
val getSpecials = foodRepository.getSpecials(limiter.toString())
foodCategory += FoodCategory(id = 1.toString(), title = "Specials", foods = getSpecials)
return foodCategories
}
}
The key ideas here:
Use viewModelScope.launch { ... } rather than applying a scope. You want everything in there that is not a coroutine to run on the main thread. See, for example, https://developer.android.com/kotlin/coroutines/coroutines-best-practices#main-safe
Return a value from getUpdatedFood so you can just update the _foodCategory value on the main thread in the viewModelScope
I have tried many different tutorials and haven't been able to relate any to my application. My application in a gist displays a user's medication that they are taking. Here is my data class...
import java.util.HashMap
class LocalMedication {
var m_medicationName: String? = null
var m_medicationQty: String? = null
var m_medicationType: String? = null
var m_medicationExpDate: String? = null
var m_medicationStatus: Boolean = false
constructor() {}
constructor(medicationName: String, medicationQty: String, medicationType: String, medicationExpDat : String, medicationStatus : Boolean) {
this.m_medicationName = medicationName
this.m_medicationType = medicationType
this.m_medicationQty = medicationQty
this.m_medicationExpDate = medicationExpDat
this.m_medicationStatus = medicationStatus
}
fun toMap(): Map<String, Any> {
val result = HashMap<String, Any>()
result.put("medicationName", m_medicationName!!)
result.put("medicationType", m_medicationType!!)
result.put("medicationQty", m_medicationQty!!)
result.put("medicationExpDate", m_medicationExpDate!!)
result.put("medicationStatus", m_medicationStatus!!)
return result
}
}
Here is my view holder class
package com.example.home_med.viewHolder
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.home_med.R
class medicationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var medicationName: TextView
var medicationType: TextView
var medicationQty: TextView
init {
medicationName = view.findViewById(R.id.rv_medicationName)
medicationType = view.findViewById(R.id.rv_medicationType)
medicationQty = view.findViewById(R.id.rv_medicationQty)
}
}
Here is my fragment
class LocalMedication : Fragment() {
private var adapter: FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>? = null
private var firestoreDB: FirebaseFirestore? = null
private var firestoreListener: ListenerRegistration? = null
private var medList = mutableListOf<LocalMedication>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
firestoreDB = FirebaseFirestore.getInstance()
val recyclerView = recyclerview as? RecyclerView
val mLayoutManager = LinearLayoutManager(context)
recyclerView?.layoutManager = mLayoutManager
recyclerView?.itemAnimator = DefaultItemAnimator()
loadMedication()
firestoreListener = firestoreDB!!.collection("notes")
.addSnapshotListener(EventListener { documentSnapshots, e ->
if (e != null) {
Log.e(TAG, "Listen failed!", e)
return#EventListener
}
medList = mutableListOf()
if (documentSnapshots != null) {
for (doc in documentSnapshots) {
val note = doc.toObject(LocalMedication::class.java)
note.m_medicationName = doc.id
medList.add(note)
}
}
adapter!!.notifyDataSetChanged()
recyclerView?.adapter = adapter
})
}
override fun onDestroy() {
super.onDestroy()
firestoreListener!!.remove()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentLocalMedicationBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_local_medication, container, false)
binding.viewMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToViewMedication())
}
binding.addMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToAddMedication())
}
binding.homeButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToHome2())
}
setHasOptionsMenu(true)
return binding.root
}
private fun loadMedication() {
val query = firestoreDB!!.collection("notes")
val response = FirestoreRecyclerOptions.Builder<LocalMedication>()
.setQuery(query, LocalMedication::class.java)
.build()
adapter = object : FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>(response) {
override fun onBindViewHolder(holder: medicationViewHolder, position: Int, model: LocalMedication) {
val note = medList[position]
holder.medicationName.text = note.m_medicationName
holder.medicationType.text = note.m_medicationType
holder.medicationQty.text = note.m_medicationQty
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): medicationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return medicationViewHolder(view)
}
override fun onError(e: FirebaseFirestoreException) {
Log.e("error", e!!.message)
}
}
adapter!!.notifyDataSetChanged()
recyclerview?.adapter = adapter
}
public override fun onStart() {
super.onStart()
adapter!!.startListening()
}
public override fun onStop() {
super.onStop()
adapter!!.stopListening()
}
}
Here is my recyclerViewItem XML file
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/rv_medicationName"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationQty"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toEndOf="#+id/rv_medicationName"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationType"
style="#style/word_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/rv_medicationQty"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
Here is my localMedications XML file
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".LocalMedication">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:text="LOCAL MEDICATION"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.478"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="543dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="#android:color/darker_gray"
app:layout_constraintBottom_toTopOf="#+id/homeButton"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView"
tools:listitem="#layout/recyclerview_item" />
<Button
android:id="#+id/viewMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100dp"
android:layout_marginBottom="100dp"
android:text="View Med"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="#+id/addMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:layout_marginBottom="100dp"
android:text="Add Medication"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/homeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="59dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="103dp"
android:text="Home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/viewMedicationButton"
app:layout_constraintStart_toEndOf="#+id/addMedicationButton"
app:layout_constraintTop_toBottomOf="#+id/addMedicationButton"
app:layout_constraintVertical_bias="0.972" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Here is what my database looks like...
Datatbase
Any help or guidance would be great. It runs the application, but in the Logs, it says "RecyclerView: No adapter attached; skipping layout"
The problem in your code lies in the fact that the names of the fields in your LocalMedication class are different than the name of the properties in your database. You have in your LocalMedication class a field named m_medicationName while in your database I see it as medicationName and this is not correct. The names must match. Behind the scene, Kotlin is creating a Java class with a getter named getM_medicationName() so Firebase is looking in the database for a field named m_medicationName and not medicationName.
There are two ways in which you can solve this problem. The first one would be to remove the data in your database and add it again using field names (m_medicationName, m_medicationQty etc) that exist in your LocalMedication class.
If you are not allowed to use the first solution, then the second approach will be to use annotations in front of your public fields. So you should use the PropertyName annotation in front of every field. So in your LocalMedication class, a field should look like this:
#get:PropertyName("medicationName")
#set:PropertyName("medicationName")
public var m_medicationName: String? = null
As explained in my answer from the following post:
I am trying to get the correct reference to my Firebase Database child and set the values in my RecyclerView but I am having some issues
It's for Firebase realtime database but the same rules apply to Cloud Firestore.
What is the best approach to validate form data using ViewModel and Databinding?
I have a simple Sign-Up activity that links binding layout and ViewModel
class StartActivity : AppCompatActivity() {
private lateinit var binding: StartActivityBinding
private lateinit var viewModel: SignUpViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, SignUpViewModelFactory(AuthFirebase()))
.get(SignUpViewModel::class.java);
binding = DataBindingUtil.setContentView(this, R.layout.start_activity)
binding.viewModel = viewModel;
signUpButton.setOnClickListener {
}
}
}
ViewModel with 4 ObservableFields and signUp() method that should validate data before submitting data to the server.
class SignUpViewModel(val auth: Auth) : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
val email: MutableLiveData<String> = MutableLiveData()
val password: MutableLiveData<String> = MutableLiveData()
val passwordConfirm: MutableLiveData<String> = MutableLiveData()
fun signUp() {
auth.createUser(email.value!!, password.value!!)
}
}
I guess we can add four boolean ObservableFields for each input, and in signUp() we can check inputs and change state of boolean ObservableField that will produce an appearing error on layout
val isNameError: ObservableField<Boolean> = ObservableField()
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
But I am not sure that ViewModel should be responsible for validation and showing an error for a user and we will have boilerplate code
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.maximdrobonoh.fitnessx.SignUpViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorGreyDark"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="#+id/appTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="#string/app_title"
android:textColor="#color/colorWhite"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/screenTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/appTitle">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="#string/sign"
android:textColor="#color/colorWhite"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/up"
android:textColor="#color/colorWhite"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:id="#+id/form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/screenTitle">
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_name"
android:inputType="textPersonName"
android:text="#={viewModel.name}" />
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_email"
android:inputType="textEmailAddress"
android:text="#={viewModel.email}"
/>
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_password"
android:inputType="textPassword"
android:text="#={viewModel.password}" />
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_confirm_password"
android:inputType="textPassword"
android:text="#={viewModel.passwordConfirm}" />
<Button
android:id="#+id/signUpButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="#drawable/button_gradient"
android:text="#string/sign_up_next_btn"
android:textAllCaps="true"
android:textColor="#color/colorBlack" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</layout>
There can be many ways to implement this. I am telling you two solutions, both works well, you can use which you find suitable for you.
I use extends BaseObservable because I find that easy than converting all fields to Observers. You can use ObservableFields too.
Solution 1 (Using custom BindingAdapter)
In xml
<variable
name="model"
type="sample.data.Model"/>
<EditText
passwordValidator="#{model.password}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={model.password}"/>
Model.java
public class Model extends BaseObservable {
private String password;
#Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}
DataBindingAdapter.java
public class DataBindingAdapter {
#BindingAdapter("passwordValidator")
public static void passwordValidator(EditText editText, String password) {
// ignore infinite loops
int minimumLength = 5;
if (TextUtils.isEmpty(password)) {
editText.setError(null);
return;
}
if (editText.getText().toString().length() < minimumLength) {
editText.setError("Password must be minimum " + minimumLength + " length");
} else editText.setError(null);
}
}
Solution 2 (Using custom afterTextChanged)
In xml
<variable
name="model"
type="com.innovanathinklabs.sample.data.Model"/>
<variable
name="handler"
type="sample.activities.MainActivityHandler"/>
<EditText
android:id="#+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:afterTextChanged="#{(edible)->handler.passwordValidator(edible)}"
android:text="#={model.password}"/>
MainActivityHandler.java
public class MainActivityHandler {
ActivityMainBinding binding;
public void setBinding(ActivityMainBinding binding) {
this.binding = binding;
}
public void passwordValidator(Editable editable) {
if (binding.etPassword == null) return;
int minimumLength = 5;
if (!TextUtils.isEmpty(editable.toString()) && editable.length() < minimumLength) {
binding.etPassword.setError("Password must be minimum " + minimumLength + " length");
} else {
binding.etPassword.setError(null);
}
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setModel(new Model());
MainActivityHandler handler = new MainActivityHandler();
handler.setBinding(binding);
binding.setHandler(handler);
}
}
Update
You can also replace
android:afterTextChanged="#{(edible)->handler.passwordValidator(edible)}"
with
android:afterTextChanged="#{handler::passwordValidator}"
Because parameter are same of android:afterTextChanged and passwordValidator.
This approach uses TextInputLayouts, a custom binding adapter, and creates an enum for form errors. The result I think reads nicely in the xml, and keeps all validation logic inside the ViewModel.
The ViewModel:
class SignUpViewModel() : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
// the rest of your fields as normal
val formErrors = ObservableArrayList<FormErrors>()
fun isFormValid(): Boolean {
formErrors.clear()
if (name.value?.isNullOrEmpty()) {
formErrors.add(FormErrors.MISSING_NAME)
}
// all the other validation you require
return formErrors.isEmpty()
}
fun signUp() {
auth.createUser(email.value!!, password.value!!)
}
enum class FormErrors {
MISSING_NAME,
INVALID_EMAIL,
INVALID_PASSWORD,
PASSWORDS_NOT_MATCHING,
}
}
The BindingAdapter:
#BindingAdapter("app:errorText")
fun setErrorMessage(view: TextInputLayout, errorMessage: String) {
view.error = errorMessage
}
The XML:
<layout>
<data>
<import type="com.example.SignUpViewModel.FormErrors" />
<variable
name="viewModel"
type="com.example.SignUpViewModel" />
</data>
<!-- The rest of your layout file etc. -->
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/text_input_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorText='#{viewModel.formErrors.contains(FormErrors.MISSING_NAME) ? "Required" : ""}'>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Name"
android:text="#={viewModel.name}"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Any other fields as above format -->
And then, the ViewModel can be called from activity/fragment as below:
class YourActivity: AppCompatActivity() {
val viewModel: SignUpViewModel
// rest of class
fun onFormSubmit() {
if (viewModel.isFormValid()) {
viewModel.signUp()
// the rest of your logic to proceed to next screen etc.
}
// no need for else block if form invalid, as ViewModel, Observables
// and databinding will take care of the UI
}
}
I've written a library for validating bindable fields of an Observable object.
Setup your Observable model:
class RegisterUser:BaseObservable(){
#Bindable
var name:String?=""
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#Bindable
var email:String?=""
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
}
Instantiate and add rules
class RegisterViewModel : ViewModel() {
var user:LiveData<RegisterUser> = MutableLiveData<RegisterUser>().also {
it.value = RegisterUser()
}
var validator = ObservableValidator(user.value!!, BR::class.java).apply {
addRule("name", ValidationFlags.FIELD_REQUIRED, "Enter your name")
addRule("email", ValidationFlags.FIELD_REQUIRED, "Enter your email")
addRule("email", ValidationFlags.FIELD_EMAIL, "Enter a valid email")
addRule("age", ValidationFlags.FIELD_REQUIRED, "Enter your age (Underage or too old?)")
addRule("age", ValidationFlags.FIELD_MIN, "You can't be underage!", limit = 18)
addRule("age", ValidationFlags.FIELD_MAX, "You sure you're still alive?", limit = 100)
addRule("password", ValidationFlags.FIELD_REQUIRED, "Enter your password")
addRule("passwordConfirmation", ValidationFlags.FIELD_REQUIRED, "Enter password confirmation")
addRule("passwordConfirmation", ValidationFlags.FIELD_MATCH, "Passwords don't match", "password")
}
}
And setup your xml file:
<com.google.android.material.textfield.TextInputLayout
style="#style/textFieldOutlined"
error='#{viewModel.validator.getValidation("email")}'
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/email"
style="#style/myEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your email"
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:text="#={viewModel.user.email}" />
What you have in mind is right, actually. The viewmodel should not know anything about the android system and will only work with pure java/kotlin. Thus, doing what you are thinking of is right. ViewModel's shouldn't know about the android system as all view interactions should be handled on the View. But, their properties can be bounded to the view.
TL;DR
This will work
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
Suggestion
I would suggest, if you would like to dig in deeper, you could use Custom Binding Adapters. This way you:
won't need additional variables to your view model
have a cleaner view model since the error handling is on the custom binding adapter
would easier read on your XML views as you could define there the validations you need
I'll let your imagination fly on how you could make the custom binding adapter only have the validations. For now, it's better to understand the basics of custom binding adapters.
Cheers
Yes, you can use your validation logic from ViewModel, because you're having your observable variables from ViewModel & your xml is also deriving data from ViewModel class also.
So, Solution :
You can create #BindingAdapter in ViewModel and bind your
button click with it. Check your validation there and do some other
stuffs also.
You can create Listener, and implement it on ViewModel to receive clicks from button and bind that listener to xml.
You can use Two-Way data binding also (Be aware of infinite loops though).
//Let's say it's your binding adapter from ViewModel
fun signUp() {
if (check validation logic) {
// Produce errors
}
// Further successful stuffs
}
I am implementing android architecture component to view buyers list and select one.
Here is my Buyer entity
#Entity
data class Buyer(#PrimaryKey var id: Long = 0, var name: String = "", var photo: String = "", var address: String = "",
#Ignore var isSelected: Boolean = false,
#SerializedName("last_update_time") var lastUpdateTime: Long = 0L) {
}
I have inserted and displayed it in recyclerview.
What I want is to know how can I display particular buyer is selected, when click on one buyer.
If I click on one buyer previous selected buyer must deselect.
Please help me to implement this.
EDIT
class BuyerAdapter(private var buyers: ArrayList<Buyer>, private val listener: View.OnClickListener) : RecyclerView.Adapter<BuyerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BuyerViewHolder {
val v = LayoutInflater.from(parent.context).inflate(
R.layout.item_buyer, parent, false)
return BuyerViewHolder(v)
}
override fun onBindViewHolder(holder: BuyerViewHolder, position: Int) {
bindView(holder, position)
}
private fun bindView(holder: BuyerViewHolder, position: Int) {
val buyer = buyers[position]
holder.setName(buyer.name)
holder.setAddress(buyer.address)
holder.loadImage(ServiceHandler.BASE_URL + buyer.photo)
if (buyer.isSelected) {
holder.setCardColor(R.color.waveBlue)
holder.setNameColor(R.color.white)
holder.setAddressColor(R.color.white)
} else {
holder.setCardColor(R.color.white)
holder.setNameColor(R.color.contentGrey)
holder.setAddressColor(R.color.contentGreyDesc)
}
holder.itemView.tag = buyer
holder.itemView.setOnClickListener(listener)
}
override fun getItemCount(): Int = buyers.size
fun refresh(newBuyers: ArrayList<Buyer>) {
this.buyers = newBuyers
notifyDataSetChanged()
}
}
And here is my adapter xml item
<?xml version="1.0" encoding="utf-8"?><!--<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"--><!--xmlns:card_view="http://schemas.android.com/apk/res-auto"--><!--android:layout_width="match_parent"--><!--android:layout_height="191dp"--><!--android:paddingTop="13dp"--><!-->-->
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/buyer_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="4dp"
android:clickable="true"
card_view:cardCornerRadius="2dp"
card_view:cardElevation="4dp"
card_view:cardPreventCornerOverlap="true">
<android.support.constraint.ConstraintLayout
android:id="#+id/rlBuyerBack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="#+id/ivLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerCrop"
android:src="#drawable/placeholder_profile_photo"
card_view:layout_constraintBottom_toBottomOf="parent"
card_view:layout_constraintLeft_toLeftOf="parent"
card_view:layout_constraintTop_toTopOf="parent" />
<in.motiontech.wave.helper.WaveTextView
android:id="#+id/tvName"
style="#style/semiBoldFont"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="Name"
android:textColor="#color/contentGrey"
android:textSize="#dimen/tSizeHeader"
card_view:layout_constraintLeft_toRightOf="#+id/ivLogo"
card_view:layout_constraintTop_toTopOf="#+id/ivLogo" />
<in.motiontech.wave.helper.WaveTextView
android:id="#+id/tvAddress"
style="#style/regularFont"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="#id/tvName"
android:layout_below="#+id/tvName"
android:layout_marginLeft="8dp"
android:layout_marginTop="2dp"
android:text="Address"
android:textColor="#color/contentGrey"
android:textSize="#dimen/tSizeDesc"
card_view:layout_constraintLeft_toRightOf="#+id/ivLogo"
card_view:layout_constraintTop_toBottomOf="#+id/tvName" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
Edit 2
Here how I added data in recyclerview
viewModel.getBuyers().observe(this, Observer<List<Buyer>> {
if (it != null) {
if (it.isEmpty()) {
showProgress()
if (CommonUtils.isInNetwork(this)) {
viewModel.getBuyerList()
} else {
CommonUtils.showNoInternetDialog(this)
}
} else {
hideProgress()
buyerAdapter?.refresh(ArrayList(it))
}
}
})
What I have done is I have updated value of livedata. You can see below code:
fun selectBuyer(buyer: Buyer?) {
if (buyer == null)
return
buyers.value?.filter { it != buyer }?.forEach { it.isSelected = false }
buyers.value?.get(buyers.value!!.indexOf(buyer))?.isSelected = true
newBuyer = buyer
}
I notice there is no need to notifyupdate, as I am using observer pattern
This is how I am inserting data into database using Room Persistence Library:
Entity:
#Entity
class User {
#PrimaryKey(autoGenerate = true)
public int id;
//...
}
Data access object:
#Dao
public interface UserDao{
#Insert(onConflict = IGNORE)
void insertUser(User user);
//...
}
Is it possible to return the id of User once the insertion is completed in the above method itself without writing a separate select query?
Based on the documentation here (below the code snippet)
A method annotated with the #Insert annotation can return:
long for single insert operation
long[] or Long[] or List<Long> for multiple insert operations
void if you don't care about the inserted id(s)
#Insert function can return void, long, long[] or List<Long>. Please try this.
#Insert(onConflict = OnConflictStrategy.REPLACE)
long insert(User user);
// Insert multiple items
#Insert(onConflict = OnConflictStrategy.REPLACE)
long[] insert(User... user);
According to the documentation functions annoted with #Insert can return the rowId.
If the #Insert method receives only 1 parameter, it can return a long, which is the new rowId for the inserted item. If the parameter is an array or a collection, it should return long[] or List<Long> instead.
The problem I have with this is that it returns the rowId and not the id and I still haven't found out how to get the id using the rowId.
Edit: I now know how to get the id from the rowId. Here is the SQL command:
SELECT id FROM table_name WHERE rowid = :rowId
Get the row ID by the following sniplet. It uses callable on an ExecutorService with Future.
private UserDao userDao;
private ExecutorService executorService;
public long insertUploadStatus(User user) {
Callable<Long> insertCallable = () -> userDao.insert(user);
long rowId = 0;
Future<Long> future = executorService.submit(insertCallable);
try {
rowId = future.get();
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return rowId;
}
Ref: Java Executor Service Tutorial for more information on Callable.
The return value of the insertion for one record will be 1 if your statement successfully.
In case you want to insert list of objects, you can go with:
#Insert(onConflict = OnConflictStrategy.REPLACE)
public long[] addAll(List<Object> list);
And execute it with Rx2:
Observable.fromCallable(new Callable<Object>() {
#Override
public Object call() throws Exception {
return yourDao.addAll(list<Object>);
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Object>() {
#Override
public void accept(#NonNull Object o) throws Exception {
// the o will be Long[].size => numbers of inserted records.
}
});
In your Dao, the insert query returns Long i.e. the rowId inserted.
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipes: CookingRecipes): Long
In your Model(Repository) class : (MVVM)
fun addRecipesData(cookingRecipes: CookingRecipes): Single<Long>? {
return Single.fromCallable<Long> { recipesDao.insertManual(cookingRecipes) }
}
In your ModelView class: (MVVM) Handle LiveData with DisposableSingleObserver.
Working sourcer reference : https://github.com/SupriyaNaveen/CookingRecipes
After a lot of struggle, I managed to solve this. Here is my solution using MMVM architecture:
Student.kt
#Entity(tableName = "students")
data class Student(
#NotNull var name: String,
#NotNull var password: String,
var subject: String,
var email: String
) {
#PrimaryKey(autoGenerate = true)
var roll: Int = 0
}
StudentDao.kt
interface StudentDao {
#Insert
fun insertStudent(student: Student) : Long
}
StudentRepository.kt
class StudentRepository private constructor(private val studentDao: StudentDao)
{
fun getStudents() = studentDao.getStudents()
fun insertStudent(student: Student): Single<Long>? {
return Single.fromCallable(
Callable<Long> { studentDao.insertStudent(student) }
)
}
companion object {
// For Singleton instantiation
#Volatile private var instance: StudentRepository? = null
fun getInstance(studentDao: StudentDao) =
instance ?: synchronized(this) {
instance ?: StudentRepository(studentDao).also { instance = it }
}
}
}
StudentViewModel.kt
class StudentViewModel (application: Application) : AndroidViewModel(application) {
var status = MutableLiveData<Boolean?>()
private var repository: StudentRepository = StudentRepository.getInstance( AppDatabase.getInstance(application).studentDao())
private val disposable = CompositeDisposable()
fun insertStudent(student: Student) {
disposable.add(
repository.insertStudent(student)
?.subscribeOn(Schedulers.newThread())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribeWith(object : DisposableSingleObserver<Long>() {
override fun onSuccess(newReturnId: Long?) {
Log.d("ViewModel Insert", newReturnId.toString())
status.postValue(true)
}
override fun onError(e: Throwable?) {
status.postValue(false)
}
})
)
}
}
In the Fragment:
class RegistrationFragment : Fragment() {
private lateinit var dataBinding : FragmentRegistrationBinding
private val viewModel: StudentViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialiseStudent()
viewModel.status.observe(viewLifecycleOwner, Observer { status ->
status?.let {
if(it){
Toast.makeText(context , "Data Inserted Sucessfully" , Toast.LENGTH_LONG).show()
val action = RegistrationFragmentDirections.actionRegistrationFragmentToLoginFragment()
Navigation.findNavController(view).navigate(action)
} else
Toast.makeText(context , "Something went wrong" , Toast.LENGTH_LONG).show()
//Reset status value at first to prevent multitriggering
//and to be available to trigger action again
viewModel.status.value = null
//Display Toast or snackbar
}
})
}
fun initialiseStudent() {
var student = Student(name =dataBinding.edName.text.toString(),
password= dataBinding.edPassword.text.toString(),
subject = "",
email = dataBinding.edEmail.text.toString())
dataBinding.viewmodel = viewModel
dataBinding.student = student
}
}
I have used DataBinding.Here is my XML:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="student"
type="com.kgandroid.studentsubject.data.Student" />
<variable
name="listener"
type="com.kgandroid.studentsubject.view.RegistrationClickListener" />
<variable
name="viewmodel"
type="com.kgandroid.studentsubject.viewmodel.StudentViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:id="#+id/nestedScrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="com.kgandroid.studentsubject.view.RegistrationFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constarintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="true">
<TextView
android:id="#+id/tvRoll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="Roll : 1"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="#+id/edName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tvRoll" />
<TextView
android:id="#+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Name:"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="#+id/edName"
app:layout_constraintEnd_toStartOf="#+id/edName"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="#+id/tvEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="#+id/edEmail"
app:layout_constraintEnd_toStartOf="#+id/edEmail"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="#+id/edEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edName" />
<TextView
android:id="#+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
android:textColor="#color/colorPrimary"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="#+id/edPassword"
app:layout_constraintEnd_toStartOf="#+id/edPassword"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="#+id/edPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edEmail" />
<Button
android:id="#+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:background="#color/colorPrimary"
android:text="REGISTER"
android:onClick="#{() -> viewmodel.insertStudent(student)}"
android:textColor="#android:color/background_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/edPassword" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</layout>
I have struggled a lot to accomplish this with asynctask as room insert and delete operation must be done in a separate thread. Finally able to do this with Single
type observable in RxJava.
Here is Gradle dependencies for rxjava:
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.0.3'
If the #Insert method receives a single parameter, it can return a long value, which is the new rowId for the inserted item.
enter link description here
#Insert
suspend fun insert(myEntity: MyEntity):Long