BindingAdapter with coroutine scope - android

I'm not sure if I'm using CoroutineScope properly inside my Binding Adapter function:
#BindingAdapter("srcByFileName")
fun setImageViewResource(imageView: ImageView, fileName: String?) {
if(fileName == null) return
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
val bitmap = ImageStorageManager.getImageFromInternalStorage(imageView.context, fileName)
withContext(Dispatchers.Main) {
imageView.setImageBitmap(bitmap)
}
}
}
I need to move the fetching of Bitmap to a different thread hence the need of coroutine. I'm just wondering if this is the correct way of doing this.

If you're using Data Binding, then your logic would probably be in your ViewModel, so it's better to pass viewModelScope to the BindingAdapter function.
Just add a third parameter of type CoroutineScope which you can pass from xml layout through the predefined viewModel object.
And in ViewModel, you can just define your scope like this:
val scope: CoroutineScope
get() = viewModelScope
And in xml:
<data>
<variable
name="viewModel"
type="ViewModelType" />
</data>
...
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcByFileName=#{viewModel.src}
app:coroutineScope="#{viewModel.scope}" />

Related

How to react on changes within a UiModel when it's exposed via LiveData by the ViewModel?

I watched this awesome talk by Florina Muntenescu on KontlinConf 2018 where she talked about how they reshaped their app architecture.
One part of the talk was how they expose a UiModel (not ViewModel) via LiveData from the ViewModel. (watch here)
She made a example similar to this:
class MyViewModel constructor(...) : ViewModel() {
private val _uiModel = MutableLiveData<UiModel>()
val uiModel: LiveData<UiModel>
get() = _uiModel
}
A view declaration for the ViewModel above could be:
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
She didn't talked about (or I missed it) how they react to property changes within the UiModel itself. How can I execute a function everytime text changes?
When having the text in separate LiveData property within the ViewModel I could use MediatorLiveData for this like:
myMediatorLiveData.addSource(text){
// do something when text changed
}
But when using the approach above the UiModel does not change instead the values of it are changed. So this here doesn't work:
myMediatorLiveData.addSource(uiModel){
// do something when text inside uiModel changed
}
So my question is how can I react on changes inside a UiModel in the ViewModel with this approach?
Thanks for advice,
Chris
I want to summarize my research regarding the topic above.
As #CommonsWars said in the comments above you can implement the field in the UiModel as ObservableFields. But after some hands on I currently prefer the approach described here.
This led me to the following code:
ViewModel:
class MyViewModel constructor(...) : ViewModel() {
val uiModel = liveData{
val UiModel uiModel = UiModel() // get the model from where ever you want
emit(uiModel)
}
fun doSomething(){
uiModel.value!!.username = "abc"
}
}
UiModel
class UiModel : BaseObservable() {
#get:Bindable // puts '#Bindable' on the getter
var text = ""
set(value) {
field = value
notifyPropertyChanged(BR.text) // trigger binding
}
val isValid: Boolean
#Bindable("text") get() { // declare 'text' as a dependency
return !text.isBlank()
}
}
Layout
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
<Button
android:id="#+id/sent_btn"
android:enabled="#{viewModel.uiModel.isValid}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I've chosen this approach because of the way we change properties of the UiModel from within a ViewModel.
We can you can set/get the username in the ViewModel by:
fun doSomething(){
uiModel.value!!.username = "abc"
}
fun doSomething(){
uiModel.value!!.username
}
When you implementing it by ObservableFields you have to set/get the username by:
fun doSomething(){
uiModel.value!!.username.set("abc")
}
fun doSomething(){
uiModel.value!!.username.get()
}
You can pick the approach which suites best for your needs!
Hope this helps someone.
Chris

Android passing query parameter to viewmodel

I have a view and viewmodel which has 2 functionalities -
1) Clicking a button in view and getting data.
2) A Spinner where you can select an item and ask the viewmodel to get data for that item as a query parameter.
I already implemented the first point like this -
MyView code -
viewModel.onGetDataClicked.observe(this, Observer {
//....
})
My ViewModel code -
private val viewState = MyViewState()
val onGetDataClicked =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
when (it) {
....
}
}
MyAction code -
sealed class MyAction : Action {
object GetDataAction : MyAction()
}
My question is how do I pass the spinner value from view to the viewmodel? Since in viewmodel I have a val onGetDataClicked and not a function.
First you should get the item value in the view itself, after that pass the item value to the required method in the ViewModel and from the ViewModel to the Repository(where you are querying the data from).
// in view
viewModel.onGetDataClicked(item:DataType).observe(this, Observer {
//....
})
//in viewmodel
private val viewState = MyViewState()
val onGetDataClicked:(item:DataType) =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
//you have item here, pass it where you require
when (it) {
....
}
}
Hi you can use the selectedItemPosition attribute from the view and pass position to viewModel, accordingly you can map the item using the position.
layout.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">
<data class="FeedbackBinding">
<variable
name="vm"
type="com.ec.service.ServiceViewModel" />
</data>
<androidx.appcompat.widget.AppCompatSpinner
android:id="#+id/unitAET"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:selectedItemPosition="#={vm.selectedUnitPosition}"
app:entries="#{vm.unitNames}"
app:layout_constraintEnd_toEndOf="#+id/textView10"
app:layout_constraintStart_toStartOf="#+id/textView10"
app:layout_constraintTop_toBottomOf="#+id/textView10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
the selectedUnitPosition is a MutableLeveData
fragment.kt
In your fragment initialise the vm (viewModel)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
withViewModel<ServiceViewModel>(factory) {
binding.vm = this
}
}

Too many XML data bindings

I made a View that I want to reuse across many pages. It contains feedback elements for the user such as a ProgressBar, TextView etc.
Due to high amount of items within, binding all those turns out like this:
<layout ... >
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout ... >
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I find copy/pasting the whole XML block messy and error-prone. Is there any way to make this simpler ?
ScreenStateObserver is just a interface that I implement in my ViewModel and bind as follows:
override fun onCreateView(...): View? {
val factory = InjectorUtils.provideViewModelFactory()
viewmodel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
binding = MyFragmentBinding.inflate(inflater, container, false).apply {
screenObserver = viewmodel
}
}
class AtoZViewModel() : ViewModel(), ScreenStateObserver { ... }
interface ScreenStateObserver {
val isProgressVisible : MutableLiveData<Boolean>
val isTextVisible : MutableLiveData<Boolean>
val isButtonVisible : MutableLiveData<Boolean>
// [..]
}
Thanks !
Here is my suggestion to reduce code.
First declare a class like this
interface ScreenState {
class Loading : ScreenState
class Error(val errorMessage: String, val errorButtonText: String) : ScreenState
}
and inside you CustomView it will be
internal class ScreenStateView {
fun setState(state: ScreenState) {
if (state is ScreenState.Loading) {
// show loading
} else {
// hide loading
}
if (state is ScreenState.Error) {
//show {state.errorMessage} and {state.errorButtonText}
} else {
// hide error
}
}
}
using in xml
<my.namespace.view.ScreenStateView
...
app:state="#{screenObserver.screenState}"
...
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" /> // for onButtonClick I think it still better if we keep like this
Hope it help
You can use <include> in data binding layouts. Included layout file can have its own data and variables that you can access from the main binding class as well.
You have to create a layout file(such as layout_state_view.xml that contains your view and data variables relevant to your view:
<layout>
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</layout>
Now you can include this in your root layout file:
<layout>
<data>
...
</data>
<LinearLayout //Can be any layout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout:="#layout/layout_state_view">
</LinearLayout>
</layout>
Now when you are using binding class, if you root layout file was R.layout.mainActivity then it would look like this:
binding.layoutStateView.setScreenObserver(...)
You can also make a variable in root layout and pass that variable to child layout by using bind tag as mentioned on documentation but since you are looking to reduce code, it would be unnecessary.
Note: Since you only have a single view, you might be tempted to use <merge> tag. Databinding's layout tag does not support merge as a direct child.
Documentation Reference:
https://developer.android.com/topic/libraries/data-binding/expressions#includes
My solution to reduce code is first define class for ScreenStateView(different properties of ScreenStateView in this class) then use it as much times as you needed

How to databinding image loading with Glide?

I am trying to load image with databinding. But I never got over it. Where's my problem? Below is my code and layout construction.
MyItemViewModel.kt
#BindingAdapter("imageUrl")
fun loadImage(view: RoundedImageView, url: String) = Glide.with(view.context).load(url).into(view)
layout.xml
<data>
<variable
name="viewModel"
type="com.myapp.app.ui.activity.albumlist.AlbumItemViewModel"/>
</data>
<com.makeramen.roundedimageview.RoundedImageView
android:layout_width="60dp"
android:id="#+id/ivRoundedAlbum"
android:layout_marginStart="#dimen/unit_20_dp"
app:riv_corner_radius="8dp"
app:imageUrl="#{viewModel.data.cover}"
android:layout_height="60dp"/>
You need to make url parameter nullable, and guard against null like this:
#BindingAdapter("imageUrl")
fun loadImage(view: RoundedImageView, url: String?) {
if (!url.isNullOrEmpty()) {
.....
}
}
BindingAdapter methods should be static, so marking it #JvmStatic would help in this case.
But that will generate 'compile time error' that "methods can't be static inside class" and so it should be moved to companion object or named objects.
In your case, you're having method in class member level so moving it to companion object will help. So for MyItemViewModel.kt make companion object and move method there like below :
class MyItemViewModel{
//Some code
companion object {
#JvmStatic
#BindingAdapter("imageUrl")
fun loadImage(view: RoundedImageView, url: String) { // This methods should not have any return type, = declaration would make it return that object declaration.
Glide.with(view.context).load(url).into(view)
}
}
//Some other code
}
Note: Also remove method declaration with =. Binding methods should have return type Unit.
Edit: One can also use method Glide.with(view) as #hmac suggested in comment, but ...
Things to consider before using this Glide.with(view):
Your view should be attached before using it from Activity/Fragment. Best usecase for this method is Custom View/ViewGroup.
Consider layout hierarchy before using this method as too many nested/large hierarchy layouts are discouraged to use that method. It becomes inefficient for such layouts.
Also note that, if view is inside non-support fragment class or context is of non-support fragment than that can produce noisy log as documentation indicates, first migrate to support library (Now considered as AndroidX) instead before using this method!
This work fine for me
<ImageView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:layout_gravity="center"
bind:image="#{subcategory.image}"
bind:placeholder="#{#drawable/no_imge}"
android:layout_weight="1" />
#BindingAdapter("image","placeholder")
fun setImage(image: ImageView, url: String?, placeHolder: Drawable) {
if (!imageUrl.isNullOrEmpty()){
Glide.with(image.context).load(url).centerCrop()
.placeholder(R.drawable.no_imge)
.into(image)
}
else{
image.setImageDrawable(placeHolder)
}
}
It would be more convenient to create a binding adapter which accepts multiple optional attributes so you can customize the loading request. Here's an example of such adapter.
#BindingAdapter(
"srcUrl",
"circleCrop",
"placeholder",
requireAll = false // make the attributes optional
)
fun ImageView.bindSrcUrl(
url: String,
circleCrop: Boolean = false,
placeholder: Drawable?,
) = Glide.with(this).load(url).let { request ->
if (circleCrop) {
request.circleCrop()
}
if (placeholder != null) {
request.placeholder(placeholder)
}
request.into(this)
}
And you can use it like this:
<ImageView
...
app:srcUrl="#{someUrl}"
app:placeholder="#{#drawable/ic_placeholder}"
app:circleCrop="#{true}" />
You can also find an example in sources of the Owl - an official Android sample app on GitHub. See BindingAdapters.kt.
#JvmStatic
#BindingAdapter("glide")
fun glide(view: ShapeableImageView, url: String?) {
if (!url.isNullOrEmpty()) {
Glide.with(view).load(url).into(view)
}
}
I think the best practise should be to create a separate variable for imageUrl of type string in layout.xml. BindingAdapter should be in model class. Also, the BindingAdapter method should be static as pointed out in comments. You can do it by wrapping inside a companion object with #JvmStatic annotation. For more details check this
<variable
name="imageUrl"
type="String" />
use app:glideSrc like this
<ImageView
android:id="#+id/sender_profile_image_view"
android:layout_width="#dimen/email_sender_profile_image_size"
android:layout_height="#dimen/email_sender_profile_image_size"
android:contentDescription="#string/email_sender_profile_content_desc"
android:scaleType="centerCrop"
app:glideCircularCrop="#{true}"
app:glideSrc="#{email.sender.avatar}"
app:layout_constraintTop_toTopOf="#id/sender_text_view"
app:layout_constraintBottom_toBottomOf="#+id/recipient_text_view"
app:layout_constraintEnd_toEndOf="parent"
tools:src="#drawable/avatar_3" />
and in BindingAdapter
#BindingAdapter(
"glideSrc",
"glideCenterCrop",
"glideCircularCrop",
requireAll = false
)
fun ImageView.bindGlideSrc(
#DrawableRes drawableRes: Int?,
centerCrop: Boolean = false,
circularCrop: Boolean = false
) {
if (drawableRes == null) return
createGlideRequest(
context,
drawableRes,
centerCrop,
circularCrop
).into(this)
}
private fun createGlideRequest(
context: Context,
#DrawableRes src: Int,
centerCrop: Boolean,
circularCrop: Boolean
): RequestBuilder<Drawable> {
val req = Glide.with(context).load(src)
if (centerCrop) req.centerCrop()
if (circularCrop) req.circleCrop()
return req
}
Layout:
<data>
<import type="com.example.package.R"/>
</data>
<ImageView
android:id="#+id/logo"
android:contentDescription="#string/logo"
android:scaleType="fitCenter"
app:gif = "#{R.drawable.logo}" />
Data Binding Utils:
#BindingAdapter("gif")
fun ImageView.setGif(res: Int) {
Glide.with(this).load(res).into(this);
}

How to pass an argument to a function in data binding?

I have two variables inside my layout file :
<data>
<variable name="createExpenseViewModel" type="com.lionosur.dailyexpenses.viewModels.MainViewModel"/>
<variable name="createExpenseConverter" type="com.lionosur.dailyexpenses.converters.createExpenseActivityConverter.Companion"/>
</data>
My view model has an method to return the live data :
fun getAllExpenseItems(): LiveData<List<Expense>> {
return expenseRepository.getAllExpenseItems()
}
I need to observe this data and populate an spinner,
class createExpenseActivityConverter {
// contains all the static methods to convert the data for the ui
companion object {
fun getExpenseCategoryListFromSource(list:List<Source>):ArrayList<String> {
val categoryItems = ArrayList<String>()
categoryItems.addAll(list.map { it.sourceName })
return categoryItems
}
}
}
to populate a spinner I need to supply an array list of string
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="#+id/expense_category"
android:entries="#{()-> createExpenseViewModel.getAllSourceItems(1) }"
app:layout_constraintStart_toStartOf="#+id/textView"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="#+id/textView" app:layout_constraintWidth_percent="0.7"
/>
in android:entries I need to convert the observed data to array list of string, how do I pass the #{()-> createExpenseViewModel.getAllSourceItems(1) } result in to another static method createExpenseViewConverter.getExpenseCategoryListFromSource(sourceList) which would return a array list of string.
in my activity i have setup binding like this
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_expense)
val mainViewModel = DaggerExpenseComponent.builder()
.setContext(this)
.build()
.getExpenseViewModel()
binding.setLifecycleOwner(this)
binding.createExpenseViewModel = mainViewModel
You'll need to use below syntax for that :
android:entries="#{createExpenseConverter.getExpenseCategoryListFromSource(createExpenseViewModel.getAllSourceItems(1))}"
Here, what we've done is accessed your input from MainViewModel object createExpenseViewModel using getAllSourceItems() method;
And then passing it to another class createExpenseActivityConverter object createExpenseConverter using method getExpenseCategoryListFromSource() which returns you ArrayList<String> that your spinner requires.
Edit:
When you use LiveData in DataBinding, Data-binding Compiler takes care of refreshing data just like ObservableFields. All you need to do is provide your LifeCycleOwner to your databinding object.
For Example:
If your activity has ViewDataBinding let's say mActivityBinding using which you provide your ViewModel to set LiveData in xml binding, then after setting your ViewModel consider setting LifecycleOwner like below code :
//Some Activity having data-binding
... onCreate() method of activity
mActivityBinding.setViewModel(myViewModel);
mAcivityBinding.setLifecycleOwner(this); // Providing this line will help you observe LiveData changes from ViewModel in data-binding.
...
Refer here

Categories

Resources