I am trying to figure out how to bind a viewmodel to a bottomsheet in a way that I can have the bottomsheet both expand, collapse, and hide using observable fields on the viewmodel.
Thank you!
You should use custom BindingAdapter.
#BindingAdapter("bottomSheetBehaviorState")
public static void setState(View v, int bottomSheetBehaviorState) {
BottomSheetBehavior<View> viewBottomSheetBehavior = BottomSheetBehavior.from(v);
viewBottomSheetBehavior.setState(bottomSheetBehaviorState);
}
Bind it in xml to your view:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
(...)
<android.support.v4.widget.NestedScrollView
android:id="#+id/group_bottom_sheet"
bottomSheetBehaviorState="#{viewModel.bottomSheetBehaviorState}"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#android:color/holo_blue_bright"
app:behavior_hideable="true"
app:behavior_peekHeight="50dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>
(...)
</layout>
And change state in ViewModel. Related code from my ViewModel:
public final ObservableInt bottomSheetBehaviorState = new ObservableInt(BottomSheetBehavior.STATE_HIDDEN);
#Override
public void onAction(boolean show){
bottomSheetBehaviorState.set(show? BottomSheetBehavior.STATE_COLLAPSED : BottomSheetBehavior.STATE_HIDDEN);
}
Related
I study the google example at https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample .And it works like this :
But I need the about fragment to take the whole screen.What is best practice?
I have try this :
activity?.supportFragmentManager?.beginTransaction()?.replace(R.id.root_activity,Detail())?.addToBackStack("About")?.commit()
and get this:
the activity_main.xml:
<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:layout_width="match_parent"
android:id="#+id/root_activity"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.android.navigationadvancedsample.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="#menu/bottom_nav"/>
I can set activity?.findViewById<BottomNavigationView>(R.id.bottom_nav)?.visibility=View.GONE
or activity?.findViewById<BottomNavigationView>(R.id.bottom_nav)?.visibility=View.INVISIBLE
But when it comes back,it looks like this,I think this is not good way:
thanks!!
You have to toggle visibility of BottomNavigationView as shown below:
NavController.OnDestinationChangedListener destinationChangedListener = new NavController.OnDestinationChangedListener() {
#Override
public void onDestinationChanged(#NonNull NavController controller, #NonNull NavDestination destination, #Nullable Bundle arguments) {
if(destination.getId() == R.id.navigation_notifications){
navView.setVisibility(View.GONE);
}else{
navView.setVisibility(View.VISIBLE);
}
}
};
navController.addOnDestinationChangedListener(destinationChangedListener);
in case anybody has ths same question,I post my answer here.
to work around ,I put BottomNavigationView into a MainFragment instead of MainActivity . And in the MainActivity dynamically instantiate the MainFragment,so I can replace the MainFragment as a whole in my deep child fragment .
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val container = FrameLayout(this)
container.id = R.id.activity_container
setContentView(container)
supportFragmentManager.beginTransaction().replace(R.id.activity_container,MainFragment()).commit()
}
}
in the nested fragment :
activity?.supportFragmentManager?.beginTransaction()?.replace(R.id.activity_container,About())?.addToBackStack("Abouts")?.commit()
final effect(have strip away the toolbar):
I have an activity which has a fragment container which loads a fragment with recyclerview. On clicking any of the recycler view items, it will open a new activity. I want to write an espresso test case for this scenario.
My Activity:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#drawable/background">
<FrameLayout
android:id="#+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1">
</FrameLayout>
My Fragment xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_items"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
MyTestCase:
#RunWith(AndroidJUnit4.class)
public class MainActivityTest {
#Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
#Before
public void init() {
mActivityTestRule.getActivity()
.getSupportFragmentManager().beginTransaction();
}
#Test
public void recyclerview_clickTest() {
/* onView(withId(R.id.fragmentContainer)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
onView(withText("Alpha")).perform(click());
onView(allOf(instanceOf(TextView.class), withParent(withResourceName("action_bar"))))
.check(matches(withText("Alpha")));*/
onData(allOf(is(new BoundedMatcher<Object, MyModel>(MyModel.class) {
#Override
public void describeTo(Description description) {
}
#Override
protected boolean matchesSafely(MyModel abc) {
return onView(allOf(instanceOf(TextView.class), withParent(withResourceName("action_bar"))))
.check(matchesSafely(withResourceName(abc)));
}
})));
}
}
TIA.
You can use ViewMatchers for this action
Example
onView(withId(R.id.your_recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
Try the following
onView(allOf(isDisplayed(), withId(R.id.your_recycler_view))).perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
If the above still does not work and you're sure that the recycler view is on display then launch UIAutomator and try to highlight above your recycler view, check if the thing it highlights is your recycler view, chances are your recycler view might have something on top of it which makes it not clickable (or it might not even be on display).
Another thing to check is if there is a transition and no animation is in place, the test might be looking for your recycler view while it is not yet on display. You might want to check on https://developer.android.com/training/testing/espresso/idling-resource
For a quicker check, just make the thread sleep prior to clicking on the recycler view
Thread.sleep(timeMs, timeout)
I have a simple databinding setup:
My ViewModel:
public class MyViewModel {
public ObservableField<Integer> viewVisibility = new ObservableField<>(View.VISIBLE);
public void buttonClicked() {
if (viewVisibility.get() == View.GONE) {
viewVisibility.set(View.VISIBLE);
} else {
viewVisibility.set(View.GONE);
}
}
}
and the layout:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.fweigl.playground.MyViewModel" />
</data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="64dp">
<View
android:visibility="#{viewModel.viewVisibility}"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#00ff00" />
<Button
android:text="click me"
android:onClick="#{() -> viewModel.buttonClicked()}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
As you can see, every click on the button switches the ObservableField<Integer> viewVisibility on the viewmodel, which in turn switches the visibility of a green rectangle. This works fine.
Now I want to do the same but using an interface as a viewmodel:
public interface IMyViewModel {
public void buttonClicked();
public ObservableField<Integer> viewVisibility = new ObservableField<>(View.VISIBLE);
}
the viewmodel:
public class MyViewModel implements IMyViewModel {
#Override
public void buttonClicked() {
if (viewVisibility.get() == View.GONE) {
viewVisibility.set(View.VISIBLE);
} else {
viewVisibility.set(View.GONE);
}
}
}
and in the layout, I import the interface instead of the implementation:
<data>
<variable
name="viewModel"
type="com.example.fweigl.playground.IMyViewModel" />
</data>
What works is the binding for the button click, buttonClicked is called and the value of viewVisibility is changed.
What doesn't work is the changing of the green rectangle view's visibility. Changes of the viewVisibility value are not reflected in the layout.
Am I doing something wrong or does databinding not (fully) work with interfaces as viewmodels?
Id you'd wrap whatever variable you'd like to bind to your view in a LiveData<>, Android will automatically unbox the data and bind it to the view
Data binding needs getter and setter to make work done, it does not access your field directly. So this will also not work without getter setter
public class MyViewModel {
public ObservableField<Integer> viewVisibility = new ObservableField<>(View.VISIBLE);
public void buttonClicked() {
if (viewVisibility.get() == View.GONE) {
viewVisibility.set(View.VISIBLE);
} else {
viewVisibility.set(View.GONE);
}
}
}
So because interface does not have getter setter, so they can not be used as model.
I am getting started for using DataBinding and something is wrong with my onClick.
GameViewModel.java
public void onClickItem(int row, int col){
Log.d("click","row: "+row+" col: "+col);
}
#BindingAdapter("load_image")
public static void loadImage(ImageView view,int imageId) {
view.setImageResource(getDrawable(imageId));
}
GameFragment.java
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//View view=inflater.inflate(R.layout.fragment_game, container, false);
FragmentGameBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_game, container, false);
View view = binding.getRoot();
ButterKnife.bind(this,view);
binding.setGameViewModel(gameViewModel);
gameViewModel= ViewModelProviders.of(getActivity()).get(GameViewModel.class);
gameViewModel.init();
return view;
}
fragment_game.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"
tools:context=".view.GameFragment">
<data>
<import type="android.support.v4.app.Fragment"/>
<import type="android.view.View"/>
<variable
name="gameViewModel"
type="harkor.addus.viewmodel.GameViewModel" />
</data>
<android.support.constraint.ConstraintLayout
(...)>
<TextView
(...)>
<android.support.constraint.ConstraintLayout
(...)>
<ImageView
android:id="#+id/image_puzzle11"
android:src="#android:color/holo_green_dark"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:onClick="#{() -> gameViewModel.onClickItem(1,1)}"
app:load_image="#{0}"
app:layout_constraintBottom_toTopOf="#+id/image_puzzle21"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toStartOf="#+id/image_puzzle12"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
(...)
load_image is working, but onClick do nothing...
No error in compilation, no crash when button is clicking on device, no result in console...
Please check with below code:
You have written as to call on Click of image as :
android:onClick="#{() -> gameViewModel.onClickItem(1,1)}"
Try to write as below and check again :
android:onClick="#{(v) -> gameViewModel.onClickItem(1,1)}"
As per the Guidance This is not the way to achieve the Architecture Principles we can work as below as per the MVVM Architecture:
1. Create an Interface
2. Define Interface as handler inside the Layout File as :
<variable
name="handler"
type="com.cityguide.interfaces.MustVisitItemListener"></variable>
3.Now we are using this handler to define onclick as :
android:onClick="#{(v) ->handler.onGalleryItemClick(v,currentPosition,photo)}"
Implement the Handler with our java Class or Activity class before bind the Handler with View as below:
private MustVisitItemListener mItemListener;
mItemListener = new MustVisitItemListener() { };
5.Set the Interface handler with bind object as below:
mbinding.setHandler(mItemListener);
The easiest way is to set the view model and calling the proper method in the View's onClick from the layout:
Your xml:
<data>
<variable
name="viewModel"
type="co.package.MyViewModel" />
</data>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/my_id"
android:layout_width="#dimen/full_width"
android:layout_height="wrap_content"
android:onClick="#{() -> viewModel.doSomething()}" />
But if for any reason you need to call a method from your fragment or activity, the best suggestion is to create an interface to handle the method, implement the method and set it to the layout as follows:
Your xml
<data>
<variable
name="myHandlers"
type="co.package.MyHandlersListener" />
</data>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/my_id"
android:layout_width="#dimen/full_width"
android:layout_height="wrap_content"
android:onClick="#{() -> myHandlers.doSomething()}" />
And within your Fragment or Activity you create the interface and then implement it:
Your activity/fragment:
/* My Handler Methods */
interface MyHandlersListener {
fun doSomething()
}
Then implement the listener, taking into consideration that the method something is defined and implemented within your activity/fragment class:
private val myHandlersListener: MyHandlersListener = object : MyHandlersListener {
override fun doSomething() {
something()
}
}
And using databinding, you can set the handler into your layout class (this can be done within the onCreate or onCreateView method depending if you are using and activity or fragment respectively):
myBinding.myHandlers = myHandlersListener
In this way it works perfectly and you follow the guide given by Android's team:
https://developer.android.com/topic/libraries/data-binding/expressions#method_references
My question is simple but I can't seem to do what I want to.
So In my activity I have this method.
public void performButtonClick(View view)
{
Log.i("INTRO", "OK");
}
Which is called from a button click event defined in xml like so
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="performButtonClick"
android:text="#string/start"/>
Now I've heard that this approach is using reflection which slows down performance so I'm trying to replace the onClick event with databinding.
I tried various combinations like
android:onClick="#{performButtonClick}"
or
android:onClick="#{(v) -> performButtonClick(v)}"
or
android:onClick="#{(v) -> MainActivity::performButtonClick}"
but None of these worked.
Could you help me out?
Ok answering to my own question.
I am new to DataBinding but I do not understand why I have to use copy-pasted solutions which make use of an accessory Handlers class and end up with more boilerplate code than I used to have.
All I wanted is an equivalent to android:onClick="performButtonClick" So here is my solution:
activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="activity"
type="com.example.mydatabinding.MainActivity"/>
</data>
<LinearLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/bg"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="#{activity::performButtonClick}"
android:text="#string/start"/>
</LinearLayout>
MainActivity.java
public final 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.setActivity(this);
}
public void performButtonClick(View view)
{
Log.i("INTRO", "OK");
}
}