I am trying to implement two MaterialCardViews that should act like a Radiogroup. So if I click one, the other should be unchecked. I am using viewModel, liveData and custom two-way data binding to save these values for later purpose (sending per email).
I had success writing the .xml and implementing the check logic, but I struggle implementing uncheck logic.
XML, short version for better visibility
<layout
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.app.data.viewmodel.EmailViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewOne"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Custom Two way databinding -->
app:state_checked="#={vm.cardOptionOneChecked}"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewTwo"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Custom Two way databinding -->
app:state_checked="#={vm.cardOptionTwoChecked}">
</com.google.android.material.card.MaterialCardView>
</layout>
ViewModel
class EmailViewModel #ViewModelInject constructor(
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// Variable for Id = cardViewOne
val cardOptionOneChecked = MutableLiveData<Boolean>()
// Variable for Id = cardViewTwo
val cardOptionTwoChecked = MutableLiveData<Boolean>()
}
CardViewAdapter.kt
#BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
if (view.isChecked != liveData.value) {
liveData.value = view.isChecked
}
}
#InverseBindingAdapter(attribute = "state_checked")
fun getStateChecked(view: MaterialCardView,): Boolean {
return view.isChecked
}
// I don't know what logic belongs here to make it work!
// Current approach just checks the current view and does nothing more. How can I save the last
// checked value?
#BindingAdapter("state_checkedAttrChanged")
fun setCheckedAttrListener(
view: MaterialCardView,
attrChange: InverseBindingListener,
) {
view.apply {
setOnClickListener { view.isChecked = true }
setOnCheckedChangeListener { card, isChecked ->
if (card.isChecked && card != view) {
card.isChecked = false
}
}
attrChange.onChange()
}
}
I appreciate every help, thank you very much!
P.S: If there is a better and easier way to achieve this e.g. telling the viewModel from the view to save isChecked, please inform me. MaterialCardView has implemented "isChecked" by default but no logic.
Okay, I've solved the Problem:
First, Change Binding Adapter
I actually don't saw any way to use two-way data binding to achieve the above written case. Here is the new Binding Adapter
// View = Clicked MaterialCard, liveData = value in viewModel
#BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
if (view.isChecked != liveData.value) {
if (liveData.value != null) {
view.isChecked = liveData.value!!
}
}
}
Second, Change XML Layout, because we don't use two-way data binding anymore
<layout
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.app.data.viewmodel.EmailViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewOne"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Deleted "=" -->
app:state_checked="#{vm.cardOptionOneChecked}"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewTwo"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Deleted "=" -->
app:state_checked="#{vm.cardOptionTwoChecked}">
</com.google.android.material.card.MaterialCardView>
</layout>
Third, Change viewmodel
class EmailViewModel #ViewModelInject constructor(
#ApplicationContext context: Context,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val cardOptionOneChecked = MutableLiveData<Boolean>()
val cardOptionTwoChecked = MutableLiveData<Boolean>()
// Added
fun firstCardClicked() {
cardOneChecked.value = true
cardTwoChecked.value = false
}
fun secondCardClicked() {
cardOneChecked.value = false
cardTwoChecked.value = true
}
}
Fourth, add clickListener to XML or Fragment (here Fragment)
cardViewOne.setOnClickListener {
viewModel.firstCardClicked()
}
cardViewTwo.setOnClickListener {
viewModel.secondCardClicked()
}
If someone has any questions, just write it in the comments, I will help.
Related
I want to set the color of a compound drawable programmatically. I think the best solution would be to use a ViewModel.
This is what I have so far:
...
<data>
<import type="androidx.core.content.ContextCompat"/>
<variable
name="viewmodel"
type="com.pezcraft.myapplication.MainViewModel" />
</data>
...
<TextView
android:id="#+id/textViewState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text"
app:drawableLeftCompat="#drawable/ic_circle"
app:drawableTint="#{ContextCompat.getColor(context, viewmodel.colorState)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textViewIP" />
public class MainViewModel extends ViewModel {
private MutableLiveData<Integer> colorState;
public MainViewModel() {
colorState = new MutableLiveData<>();
}
public MutableLiveData<Integer> getColorState() {
return colorState;
}
}
But it doesn't work when building...
[databinding] {"msg":"Cannot find a setter for \u003candroid.widget.TextView app:drawableTint\u003e that accepts parameter type \u0027int\u0027\n\nIf a binding adapter provides the setter, check that the adapter is annotated correctly and that the parameter type matches."
With Android data-binding framework, I understand that you can pass an object that extends baseObservable to the layout xml, use #Bindable on getters and do notifyPropertyChanged(BR.xxx) to have the related part re-evaluated.
What I don't understand is this: if you don't use most the stuff above and just call the getter directly in xml, when would it be evaluated?
Here's the code:
my_widget.xml
<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="someViewModel"
type="com.example.SomeViewModel" />
</data>
<androidx.cardview.widget.CardView>
<View
android:id="#+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="#{someViewModel.getName() ? View.VISIBLE : View.GONE}" />
</androidx.cardview.widget.CardView>
</layout>
MyView.java
MyWidgetBinding binding = MyWidgetBinding.inflate(LayoutInflater.from(mContext), parent, false);
binding.setSomeViewModel(someViewModel);
Questions:
If someViewModel.name ever changes, does the testView's visibility refreshes?
When does someViewModel.getName() get evaluated or how often?
If the expression is more complicated, something like:
android:visibility="#{func(otherVariable, someViewModel.getName()) ? View.VISIBLE : View.GONE}", say otherVariable is another variable defined in data section above, if somehow otherVariable gets re-set, then someViewModel.getName() will get evaluated and testView will reflect the latest visibility value, correct?
Following up on question 3, if otherVariable is changed to otherVariable.a where a is a 'bindable' field and notifyPropertyChanged(BR.a) is called in otherVariable then someViewModel.getName() will also get re-evaluated and testView will reflect the latest visibility value, correct?
Additionally, if I re-set someViewModel by calling binding.setSomeViewModel() but pass in the SAME someViewModel instance, does it do anything? Will the expression get re-evaluated?
I suggest you to create custom binding adapter for mutable visibility and use LiveData to update the visibility.
Code :
#BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Boolean>) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if (parentActivity != null) {
visibility.observe(
parentActivity,
Observer { value -> if (value) view.visibility = View.VISIBLE
else view.visibility = View.GONE})
}
}
To get the parent activity create ActivityExtensions.kt file and add the following function in it:
fun View.getParentActivity(): AppCompatActivity?{
var context = this.context
while (context is ContextWrapper) {
if (context is AppCompatActivity) {
return context
}
context = context.baseContext
}
return null
}
And in the ViewModel :
//Other code here...
val itemVisibility = MutableLiveData<Boolean>()
//Other logic here to init itemVisible
if(itemVisibile) itemVisibility.value = true else itemVisibility.value = false
And finaly the layoutItem :
<View
android:id="#+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:mutableVisibility ="#{viewModel.itemVisibility}" />
If someViewModel.name ever changes, does the testView's visibility
refreshes?
it depends on the underlying technology you are using. LiveData? Yes BaseObservable you have to manually notify that the observed property changed.
When does someViewModel.getName() get evaluated or how often?
LiveData ? when you set/post a value. BaseObservable when you notify it
Same as point 2
If you change the underlying value and notify this change properly, it will get propagated accordingly. If you change the instance of the observed object it will not.
In my xml I set visiblity condition for control as follows:
android:visibility="#{event.isMessage?(event.dateEventText!=null? View.VISIBLE:View.GONE):View.VISIBLE}"
So, if event.isMessage is true, this: (event.dateEventText!=null? View.VISIBLE:View.GONE) should be evalueated, otherwise, View.VISIBE should be returned.
But data binding throws error message:
****/ data binding error ****msg:Cannot find the setter for attribute 'android:visibility' with parameter type boolean on
android.widget.TextView
Does anybody know what's wrong?
Try this
.
.
.
android:visibility="#{event.isMessage && event.dateEventText!=null ? View.VISIBLE : View.GONE}"
.
.
.
I checked this approach because it looks okay. and it works. However you can check getter setter of Model, View class import in XML.
Following Code Works Well.
Event.class
public class Event {
boolean isMessage;
String dateEventText;
public boolean isMessage() {
return isMessage;
}
public void setMessage(boolean message) {
isMessage = message;
}
public String getDateEventText() {
return dateEventText;
}
public void setDateEventText(String dateEventText) {
this.dateEventText = dateEventText;
}
}
layout_text.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="event"
type="com.innovanathinklabs.sample.data.Event" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{event.isMessage?(event.dateEventText!=null? View.VISIBLE:View.GONE):View.VISIBLE}" />
</layout>
Suggestion:
Move your logical part in Handler.
1. Create Handler
EventHandler.class
public class EventHandler {
private Event event;
public EventHandler(Event event) {
this.event = event;
}
public int getTextVisibility() {
if (event.isMessage && event.dateEventText != null) return View.VISIBLE;
else return View.GONE;
}
}
2. Import Handler in Layout
<variable
name="handler"
type="com.innovanathinklabs.sample.data.EventHandler" />
3. Set handler value from activity
activity.setHandler(new EventHandler(yourEventModel))
4. Use handler method to set visibility
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{handler.textVisibility}" />
That's all!
Another Approach
If you don't want add new class of Handler. You can also place visibility method in model class.
1. Put getTextVisibility method in Model
public class Event{
// other variables
public int getTextVisibility() {
if (event.isMessage && event.dateEventText != null) return View.VISIBLE;
else return View.GONE;
}
}
2. Use in layout
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{event.textVisibility}" />
You can have boolean to int conversion adapter. If it's static (the same way as BindingAdapter), it will convert boolean fields which expect integer (e.g. View.VISIBLE).
#BindingConversion
int convertBooleanToVisibility(boolean isVisible) {
return isVisible ? View.VISIBLE : View.GONE;
}
In XML you'd use your method which return boolean for visibiliy:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{event.isMessageVisible()}" />
I have an activity which has a NavHostFragment. The activity receives certain values in its intent. I want to pass this data to the first fragment i.e startDestination of the navigation graph. I couldn't find any documentation regarding this.
I have already gone through this question on SO but I can't seem to find the addDefaultArguments method for navController.getGraph().
Is it possible to pass bundle to startDestination?
Answering my own question as I found the correct approach in the updated Navigation documentation.
At the time of writing this answer, I am using Navigation 2.2.0-alpha01
If you want to pass some data to the start destination directly as arguments from host activity, you need to manually set your host’s navigation graph inside the host activity’s onCreate() method, as shown below:
Get you navController:
val navController by lazy { findNavController(R.id.<your_nav_host_id>) }
Then in the host activity's onCreate()
val bundle = Bundle()
bundle.putString("some_argument", "some_value")
navController.setGraph(R.navigation.<you_nav_graph_xml>, bundle)
Or if you want to pass the whole intent extras as it is to the startDestination:
navController.setGraph(R.navigation.<you_nav_graph_xml>, intent.extras)
Since intent.extras would return a Bundle only
When you are setting the navGraph using setGraph() method, you should avoid setting the app:NavGraph attribute in
the NavHostFragment definition, because doing so results in inflating
and setting the navigation graph twice.
While reading these arguments in your startDestination fragment:
If you are using the Safe Args Plugin (which is very much recommended), then in your fragment:
private val args by navArgs<DummyFragmentArgs>()
Safe Args plugin would generate an Args class by appending Args to your fragment name. For example, if you fragment is called DummyFragment then Safe Args would generate a class called DummyFragmentArgs
where navArgs<> is an extension function defined in Android KTX
If you are not using Android KTX, you can get the args object like:
val args = DummyFragmentArgs.fromBundle(arguments!!)
Once you've acquired the arguments object, you can simply fetch your arguments:
args.someArgument
Notice how we passed "some_argument" as argument, and we are reading it as someArgument using Safe Args
If you are not using Safe Args (there is no reason to not use it though), you can access your arguments like this:
arguments?.getString("some_argument")
All of this is documented in Migrate to Navigation Component documentation here:
https://developer.android.com/guide/navigation/navigation-migrate#pass_activity_destination_args_to_a_start_destination_fragment
I found the solution after some research. It works with the latest Navigation library release. Refer the below code:
Add this in your activity layout. Note: We are not setting app:navGraph argument in the xml file. We will set it dynamically.
<fragment
android:id="#+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true" />
In your activity java file, write the below code and make changes accordingly. Use NavArgument to pass your argument value and add the argument to your custom Navgraph and then set graph.
public class YourActivity extends AppCompatActivity {
private NavArgument nameArg, mailArg;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.profile);
nameArg = new NavArgument.Builder().setDefaultValue("your name").build();
mailArg = new NavArgument.Builder().setDefaultValue("your email id").build();
NavController navController = Navigation.findNavController(this, R.id.fragment);
NavInflater navInflater = navController.getNavInflater();
NavGraph navGraph = navInflater.inflate(R.navigation.nav_profile_graph);
navGraph.addArgument("your name key", nameArg);
navGraph.addArgument("your mail key", mailArg);
navController.setGraph(navGraph);
}
}
Write the navigation graph below and add the same argument keys to the starting fragment.
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="#+id/nav_graph"
app:startDestination="#+id/profile_basic">
<fragment
android:id="#+id/profile_basic"
android:name="com.yourpackage.ProfileBasicFragment"
android:label="Profile Basic"
tools:layout="#layout/fragment_profile_basic">
<argument android:name="your name key"
app:argType="string"/>
<argument android:name="your mail key"
app:argType="string"/>
</fragment>
</navigation>
In your fragment, just fetch the values using getArguments() function.
String name = getArguments().getString("your name key");
String mail = getArguments().getString("your mail key");
i also came across same issue,
This is how i resolved it:
Remove the the xml setup of NavHostFragment from your_activity.xml : i.e remove app:navGraph="#navigation/nav_graph
This is how your XML Should look like.
<fragment
android:id="#+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
/>
Add Setup for NavHostFragment Programatically in onCreate() of activity.
And pass bundle data using NavGraph.addDefaultArguments(bundleData) api
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.editor_layout)
setupNavigation()
}
private fun setupNavigation() {
val navHostFragment = nav_host as NavHostFragment
val navController = navHostFragment.navController
val navInflater = navController.navInflater
val graph = navInflater.inflate(R.navigation.nav_graph)
graph.addDefaultArguments(intent!!.extras!!) // This is where you pass the bundle data from Activity to StartDestination
navHostFragment.navController.graph = graph
}
UPDATE:
Dependencies in my Project Gradle file:
dependencies {
def nav_version = "1.0.0-alpha08"
implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin}
}
NOTE: In Navigation Component version 1.0.0-alpha09 for some reason google have no method as addDefaultArguments() might be fixed soon. But lower version's have addDefaultArguments() method.I have checked both in java and kotlin so try using 1.0.0-alpha07 or 1.0.0-alpha08
I checked the source code an saw that there a lot of changes regarding navigation destination and arguments.
I think the proper way to pass arguments to start destination is using 'addArgument' method, something like this:
val argument1 = 1 //First value
val argument2 = "Value" //Second value
val navArgument1=NavArgument.Builder().setDefaultValue(argument1).build()
val navArgument2=NavArgument.Builder().setDefaultValue(argument2).build()
navController.getGraph().addArgument("Key1",navArgument1)
navController.getGraph().addArgument("Key2",navArgument2)
Maybe there is a better way, but i didn't found one.
Though it is a late answer there is a very easy way for passing bundles using navigation controller/Navigation Architecture:
Fragment that will send data
//Best to put in onViewCreated
val navController = findNavController()
//Put in code where you want to start navigation
val b = Bundle()
b.putString("key_1", "Data 1")
b.putString("key_2", "Data 2")
navController.navigate(R.id.action_signupFragment_to_signinFragment,b)
Fragment that will receive data
val data_1: String = arguments?.getString("key_1")?:""
val data_1: String = arguments?.getString("key_2")?:""
Depending on the type of data you are sending you need to update the safe call or make the variables nullable.
I cannot find this method, too. It is not existant in the architecture components documentation.
But the is another way to set arguments to the start destination:
// Kotlin Code, in Fragment
with(findNavController().graph) {
get(startDestination).addArgument(...)
}
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = container as NavHostFragment
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.main_nav)
var data : Data = intent.getParcelableExtra("DATA") as Data
var bundle : Bundle = Bundle()
bundle.putParcelable("DATA", data)
graph.addDefaultArguments(bundle)
graph.addDefaultArguments(intent!!.extras!!)
navHostFragment.navController.graph = graph
}
}
Add the above code in Activity for sending the data using using navigation
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
var data = NavHostFragment.findNavController(this).graph.defaultArguments.getParcelable("DATA") as Booking
}
Add the above code in fragment side
you can add arguments in your graph like this
<fragment
android:id="#+id/exampleFragment"
android:label="example_fragment"
android:name="com.example.yourapp.ui.ExampleFragment"
tools:layout="#layout/example_fragment">
<argument
android:name="exampleArgs"
app:argType="reference"
android:defaultValue="#string/example"/>
</fragment>
https://developer.android.com/guide/navigation/navigation-pass-data
Navigation 1.0.0
val navHostFragment = root_nav_host_fragment as NavHostFragment
val navController = navHostFragment.navController
val navInflater = navController.navInflater
val graph = navInflater.inflate(R.navigation.navigation)
val sectionId = intent.getIntExtra(KEY_SECTION_ID, -1)
val bundle = bundleOf(KEY_SECTION_ID to sectionId)
navHostFragment.navController.setGraph(graph, bundle)
I just run into a problem, using android databinding library.
Here is the 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>
<import type="com.test.app.ObservableFieldWrapper"/>
<variable
name="org"
type="ObservableFieldWrapper"/>
</data>
<LinearLayout
android:id="#+id/headerListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.test.app.NSpinner
android:id="#+id/orgSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:org="#{org.getSilent ? org.content : "silent"}"/>
</LinearLayout>
Here is my NSpinner:
public class ObservableFieldWrapper{
private final ObservableBoolean silent;
private final ObservableField<String> content;
#BindingAdapter("org")
public static void setOrg(Spinner view, String org) {
assert org != null;
if (org.equals("silent")) {
Log.i("ObsWrapper", "SET ORG called via binding adapter but got denied, because of SILENCE");
} else {
Log.i("ObsWrapper", "SET ORG called via binding adapter NORMALLY");
view.setSelection(Cache.GetOrgIndexForSpinner(), true);
}
}
public ObservableFieldWrapper(String startValue) {
content = new ObservableField<>(startValue);
silent = new ObservableBoolean();
silent.set(false);
}
public void setContent(String newValue) {
silent.set(false);
content.set(newValue);
content.notifyChange();
}
public void setContentSilent(String newValue) {
silent.set(true);
content.set(newValue);
}
//Bunch of getters
}
And this call should invoke the static getter provided, by ObservableFieldWrapper class (assume, that all bindings were already set):
ObservableFieldWrapper someField = new ObservableFieldWrapper("someString");
someField.setContent("some other string");
Well, problem is... It invokes nothing. But if I change my xml part from
app:org="#{org.getSilent ? org.content : "silent"}"
to common
app:org="#{org.content}"
It starts working! I realy need this extra functionality with boolean, and I am really lost trying to find the issue.
Found a work around, where didn't use any logics in xml expressions, I just passed 2 parameters to my function and did all job there.
#Bindable ("{org, silent}")
Yet, the question remains unanswered.
As George Mount mentioned - it's important to remove any getters on observable fields, otherwise it won't work, I spent pretty much time with this issue and then mentioned that I have a getter, after removing it - everything started working.