web-dev-qa-db-ja.com

Android双方向バインディングによるスピナー設定の選択

いくつかの機能がAndroidスピナーを双方向データバインディングで構成されている場合に機能させるのに苦労しています。スピナーの初期値をAndroid:selectedItemPosition。スピナーエントリはViewModelによって初期化され、正しく入力されているため、データバインディングは正しく機能しているように見えます。

問題は、selectedItemPositionの双方向バインディングにあります。変数はViewModelによって5に初期化されますが、スピナーの選択されたアイテムは0(最初のアイテム)のままです。デバッグ時に、ObservableIntの値は最初は5(設定どおり)ですが、executeBindingsの2番目のフェーズでゼロにリセットされます。

任意の助けいただければ幸いです。

test_spinner_activity.xml

<layout xmlns:tools="http://schemas.Android.com/tools"
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto">

    <data>
        <variable name="viewModel"
                  type="com.aapp.viewmodel.TestSpinnerViewModel"/>
    </data>
    <LinearLayout Android:layout_width="match_parent"
                  Android:layout_height="wrap_content">
       <Android.support.v7.widget.AppCompatSpinner
            Android:layout_width="wrap_content"
            Android:layout_height="match_parent"
            Android:id="@+id/sTimeHourSpinner"
            Android:selectedItemPosition="@={viewModel.startHourIdx}"
            Android:entries="@{viewModel.startTimeHourSelections}"/>
    </LinearLayout>
</layout>

TestSpinnerViewModel.Java

public class TestSpinnerViewModel {
    public final ObservableArrayList<String> startTimeHourSelections = new ObservableArrayList<>();
    public final ObservableInt startHourIdx = new ObservableInt();

    public TestSpinnerViewModel(Context context) {
        this.mContext = context;

        for (int i=0; i < 24; i++) {
            int hour = i;
            startTimeHourSelections.add(df.format(hour));
        }
        startHourIdx.set(5);
    }
}

TestSpinnerActivity.Java

public class TestSpinnerActivity extends AppCompatActivity {
    private TestSpinnerActivityBinding binding;
    private TestSpinnerViewModel mTestSpinnerViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.bind(findViewById(R.id.test_spinner));
        mTestSpinnerViewModel = new TestSpinnerViewModel(this);
        binding.setViewModel(mTestSpinnerViewModel);
    }

Android Studio 2.2.2を使用しており、データバインディングが有効になっています。

14
Adrian Medioli

ご提案ありがとうございます。しかし、私は自分の質問に対する答えを見つけました。 Android:selectedItemPosition=@={viewModel.startHourIdx}変数は、初期化された値5から0にリセットされていました。これは、selectedItemPositionおよびentries属性の宣言順序が原因です。私の例では、それらはその特定の順序で宣言され、自動生成されたバインディングコードは同じ順序で初期化を生成します。

したがって、selectedItemPositionが正しく設定されていても、entriesの初期化により、ArrayAdapterがインスタンス化され、selectedItemPositionが0にリセットされます。

したがって、修正は、レイアウトファイル内の2つの属性宣言を交換することです。

<data>
    <variable name="viewModel"
              type="com.aapp.viewmodel.TestSpinnerViewModel"/>
</data>
<LinearLayout Android:layout_width="match_parent"
              Android:layout_height="wrap_content">
   <Android.support.v7.widget.AppCompatSpinner
        Android:layout_width="wrap_content"
        Android:layout_height="match_parent"
        Android:id="@+id/sTimeHourSpinner"
        Android:entries="@{viewModel.startTimeHourSelections}"
        Android:selectedItemPosition="@={viewModel.startHourIdx}"/>
</LinearLayout>
18
Adrian Medioli

私は最近 GitHub にデモアプリを作成して、bindingAdapterおよびInverseBindingAdapterメカニズムを利用してスピナーで双方向のデータバインディングを実現する方法を示しました。

このアプリでは、「Android:selectedItemPosition」属性をバインドしていませんが、以下のスニペットに示すように、スピナーの選択されたアイテム自体(ObservableFieldクラスを使用)をバインドしています。これは双方向バインディングであるため、スピナーアダプターのセットアップ中にバインドされたObservableField(つまり、選択されたアイテム)に初期値を割り当て、スピナーのbindingAdapter内で特別な処理を行うことにより、スピナーの初期選択を実現できます。

詳細については、デモアプリ こちら をご確認ください。

acivity_main.xml

<layout xmlns:Android="http://schemas.Android.com/apk/res/Android"
        xmlns:tools="http://schemas.Android.com/tools"
        xmlns:app="http://schemas.Android.com/apk/res-auto"
        xmlns:bind="http://schemas.Android.com/apk/res-auto">

    <data>
        <variable
            name="bindingPlanet"
            type="au.com.chrisli.spinnertwowaydatabindingdemo.BindingPlanet"/>
        <variable
            name="spinAdapterPlanet"
            type="Android.widget.ArrayAdapter"/>
    </data>

    <RelativeLayout
        Android:id="@+id/activity_main"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        ...>

        <Android.support.v7.widget.AppCompatSpinner
            Android:id="@+id/spin"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:layout_centerInParent="true"
            style="@style/Base.Widget.AppCompat.Spinner.Underlined"
            bind:selectedPlanet="@={bindingPlanet.obvSelectedPlanet_}"
            app:adapter="@{spinAdapterPlanet}"/>

        ...(not relevant content omitted for simplicity)
    </RelativeLayout>

</layout>

BindingPlanet.Javaのバインディングアダプタ内の特別な処理

public final ObservableField<Planet> obvSelectedPlanet_ = new ObservableField<>(); //for simplicity, we use a public variable here

private static class SpinPlanetOnItemSelectedListener implements AdapterView.OnItemSelectedListener {

    private Planet initialSelectedPlanet_;
    private InverseBindingListener inverseBindingListener_;

    public SpinPlanetOnItemSelectedListener(Planet initialSelectedPlanet, InverseBindingListener inverseBindingListener) {
        initialSelectedPlanet_ = initialSelectedPlanet;
        inverseBindingListener_ = inverseBindingListener;
    }

    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
        if (initialSelectedPlanet_ != null) {
            //Adapter is not ready yet but there is already a bound data,
            //hence we need to set a flag so we can implement a special handling inside the OnItemSelectedListener
            //for the initial selected item
            Integer positionInAdapter = getPlanetPositionInAdapter((ArrayAdapter<Planet>) adapterView.getAdapter(), initialSelectedPlanet_);
            if (positionInAdapter != null) {
                adapterView.setSelection(positionInAdapter); //set spinner selection as there is a match
            }
            initialSelectedPlanet_ = null; //set to null as the initialization is done
        } else {
            if (inverseBindingListener_ != null) {
                inverseBindingListener_.onChange();
            }
        }
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {}
}

@BindingAdapter(value = {"bind:selectedPlanet", "bind:selectedPlanetAttrChanged"}, requireAll = false)
public static void bindPlanetSelected(final AppCompatSpinner spinner, Planet planetSetByViewModel,
                                      final InverseBindingListener inverseBindingListener) {

    Planet initialSelectedPlanet = null;
    if (spinner.getAdapter() == null && planetSetByViewModel != null) {
        //Adapter is not ready yet but there is already a bound data,
        //hence we need to set a flag in order to implement a special handling inside the OnItemSelectedListener
        //for the initial selected item, otherwise the first item will be selected by the framework
        initialSelectedPlanet = planetSetByViewModel;
    }

    spinner.setOnItemSelectedListener(new SpinPlanetOnItemSelectedListener(initialSelectedPlanet, inverseBindingListener));

    //only proceed further if the newly selected planet is not equal to the already selected item in the spinner
    if (planetSetByViewModel != null && !planetSetByViewModel.equals(spinner.getSelectedItem())) {
        //find the item in the adapter
        Integer positionInAdapter = getPlanetPositionInAdapter((ArrayAdapter<Planet>) spinner.getAdapter(), planetSetByViewModel);
        if (positionInAdapter != null) {
            spinner.setSelection(positionInAdapter); //set spinner selection as there is a match
        }
    }
}

@InverseBindingAdapter(attribute = "bind:selectedPlanet", event = "bind:selectedPlanetAttrChanged")
public static Planet captureSelectedPlanet(AppCompatSpinner spinner) {
    return (Planet) spinner.getSelectedItem();
}
1
chrisli