Android implement search with view model and live

2020-02-09 18:53发布

问题:

I'm working on a project in android for a udacity course I'm currently trying to implement a search function while adhering to android architecture components and using firestore and room I'm fairly new to all these concepts so please point out anything that seems wrong.

So I made a database repository to keep my firestore and room databases in sync and to deliver the data. I'm then using viewmodel and the observer pattern (I think) so my observer gets the data and looks for changes gives it to my adapter (refreshMyList(List)) which populates a recyclerview like this :

 contactViewModel = ViewModelProviders.of(this).get(ContactsViewModel.class);
 contactViewModel.getAllContacts().observe(this, new 
 Observer<List<DatabaseContacts>>() {
        @Override
        public void onChanged(@Nullable List<DatabaseContacts> 
        databaseContacts) {
            ArrayList<DatabaseContacts> tempList = new ArrayList<>();
            tempList.addAll(databaseContacts);
            contactsAdapter.refreshMyList(tempList);
            if (tempList.size() < 1) {
                results.setVisibility(View.VISIBLE);
            } else {
                results.setVisibility(View.GONE);
            }
        }
    });

I now want to perform a search of the data, I have my room queries all set up fine and I have methods in my data repository to get contacts based on a search string but I cant seem to refresh my list I've read that there are ways to do it like Transformations.switchMap ? but i cant seem to wrap my head around how it works can anyone help me

Currently I'm trying to return a List of results from an async task, it used to return live data but I changed it as getValue() was always null, not sure if that's correct, heres the async :

private static class searchContactByName extends AsyncTask<String, Void, 
ArrayList<DatabaseContacts>> {

    private LiveDatabaseContactsDao mDao;

    searchContactByName(LiveDatabaseContactsDao dao){
        this.mDao = dao;
    }

    @Override
    protected ArrayList<DatabaseContacts> doInBackground(String... params) {
        ArrayList<DatabaseContacts> contactsArrayList = new ArrayList<>();
        mDao.findByName("%" + params[0] + "%");
        return contactsArrayList;
    }
}

I call this from my contacts repository in its own sort of wrapper :

public List<DatabaseContacts> getContactByName(String name) throws 
ExecutionException, InterruptedException {
    //return databaseContactsDao.findByName(name);
    return new searchContactByName(databaseContactsDao).execute(name).get();
}

and this is called from my view model like this :

public List<DatabaseContacts> getContactByName(String name) throws 
ExecutionException, InterruptedException {
    return  contactRepository.getContactByName(name);
}

I'm then calling this from my fragment :

private void searchDatabase(String searchString) throws ExecutionException, 
InterruptedException {
    List<DatabaseContacts> searchedContacts = 
    contactViewModel.getContactByName("%" + searchString + "%");
    ArrayList<DatabaseContacts> contactsArrayList = new ArrayList<>();
    if (searchedContacts !=  null){
        contactsArrayList.addAll(searchedContacts);
        contactsAdapter.refreshMyList(contactsArrayList);
    }
}

and this is called from an on search query text changed method in my onCreateOptionsMenu :

        @Override
        public boolean onQueryTextChange(String newText) {
            try {
                searchDatabase(newText);
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return false;
        }

but it just does nothing my original recyclerview contents never change any ideas?

回答1:

you can use Transformation.switchMap to do search operations.

  1. In viewmodel create MutableLiveData which has latest search string.

  2. Inside viewmodel use:

    LiveData<Data> data = 
    LiveDataTransformations.switchMap(searchStringLiveData, string ->  
    repo.loadData(string)))
  1. Return the above live data to activity so it can observe and update view.


回答2:

I faced the same issue and I managed to fix it using

switchMap

and

MutableLiveData

We just need to use MutableLiveData to set the current value of editText, and when the user search we call setValue(editText.getText())

 public class FavoriteViewModel extends ViewModel {
            public LiveData<PagedList<TeamObject>> teamAllList;
        public MutableLiveData<String> filterTextAll = new MutableLiveData<>();

        public void initAllTeams(TeamDao teamDao) {
            this.teamDao = teamDao;
            PagedList.Config config = (new PagedList.Config.Builder())
                    .setPageSize(10)
                    .build();

            teamAllList = Transformations.switchMap(filterTextAll, input -> {
                if (input == null || input.equals("") || input.equals("%%")) {
//check if the current value is empty load all data else search
                    return new LivePagedListBuilder<>(
                            teamDao.loadAllTeam(), config)
                            .build();
                } else {
                    System.out.println("CURRENTINPUT: " + input);
                    return new LivePagedListBuilder<>(
                            teamDao.loadAllTeamByName(input), config)
                            .build();
                }

            });

            }

    }

in Activity of fragment

viewModel = ViewModelProviders.of(activity).get(FavoriteViewModel.class);
                        viewModel.initAllTeams(AppDatabase.getInstance(activity).teamDao());
                        FavoritePageListAdapter adapter = new FavoritePageListAdapter(activity);
                        viewModel.teamAllList.observe(
                                activity, pagedList -> {
                                    try {
                                        Log.e("Paging ", "PageAll" + pagedList.size());

                                        try {
                                            //to prevent animation recyclerview when change the list
                                            recycleFavourite.setItemAnimator(null);
                                            ((SimpleItemAnimator) Objects.requireNonNull(recycleFavourite.getItemAnimator())).setSupportsChangeAnimations(false);

                                        } catch (Exception e) {
                                        }

                                        adapter.submitList(pagedList);

                                    } catch (Exception e) {
                                    }
                                });
                        recycleFavourite.setAdapter(adapter);

//first time set an empty value to get all data
                        viewModel.filterTextAll.setValue("");



                edtSearchFavourite.addTextChangedListener(new TextWatcher() {
                    @Override
                    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

                    }

                    @Override
                    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

                    @Override
                    public void afterTextChanged(Editable editable) {
                      //just set the current value to search.
                        viewModel.filterTextAll.setValue("%" + editable.toString() + "%");


                    }
                });

Room Dao

@Dao
public interface TeamDao {


        @Query("SELECT * FROM teams order by orders")
        DataSource.Factory<Integer, TeamObject> loadAllTeam();


        @Query("SELECT * FROM teams where team_name LIKE  :name or LOWER(team_name_en) like LOWER(:name) order by orders")
        DataSource.Factory<Integer, TeamObject> loadAllTeamByName(String name);


    }

PageListAdapter

public class FavoritePageListAdapter extends PagedListAdapter<TeamObject, FavoritePageListAdapter.OrderHolder> {
    private static DiffUtil.ItemCallback<TeamObject> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<TeamObject>() {
                // TeamObject details may have changed if reloaded from the database,
                // but ID is fixed.
                @Override
                public boolean areItemsTheSame(TeamObject oldTeamObject, TeamObject newTeamObject) {
                    System.out.println("GGGGGGGGGGGOTHERE1: " + (oldTeamObject.getTeam_id() == newTeamObject.getTeam_id()));
                    return oldTeamObject.getTeam_id() == newTeamObject.getTeam_id();
                }

                @Override
                public boolean areContentsTheSame(TeamObject oldTeamObject,
                                                  @NonNull TeamObject newTeamObject) {
                    System.out.println("GGGGGGGGGGGOTHERE2: " + (oldTeamObject.equals(newTeamObject)));
                    return oldTeamObject.equals(newTeamObject);
                }
            };

    private Activity activity;

    public FavoritePageListAdapter() {
        super(DIFF_CALLBACK);
    }

    public FavoritePageListAdapter(Activity ac) {
        super(DIFF_CALLBACK);
        this.activity = ac;

    }

    @NonNull
    @Override
    public OrderHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_favourite, parent, false);
        return new FavoritePageListAdapter.OrderHolder(view);

    }

    @Override
    public void onBindViewHolder(@NonNull OrderHolder holder,
                                 int position) {
        System.out.println("GGGGGGGGGGGOTHERE!!!");

        if (position <= -1) {
            return;
        }
        TeamObject teamObject = getItem(position);


        try {
                holder.txvTeamRowFavourite.setText(teamObject.getTeam_name());


        } catch (Exception e) {
            e.printStackTrace();
        }


    }

    public class OrderHolder extends RecyclerView.ViewHolder {

        private TextView txvTeamRowFavourite;


        OrderHolder(View itemView) {
            super(itemView);
            txvTeamRowFavourite = itemView.findViewById(R.id.txv_team_row_favourite);
        }

    }
}


回答3:

I faced the same issue and solved it with the answer of @Rohit, thanks! I simplified my solution a bit to illustrate it better. There are Categories and each Category has many Items. The LiveData should only return items from one Category. The user can change the Category and then the fun search(id: Int) is called, which changes the value of a MutableLiveData called currentCategory. This then triggers the switchMap and results in a new query for items of the category:

class YourViewModel: ViewModel() {

    // stores the current Category
    val currentCategory: MutableLiveData<Category> = MutableLiveData()

    // the magic happens here, every time the value of the currentCategory changes, getItemByCategoryID is called as well and returns a LiveData<Item>
    val items: LiveData<List<Item>> = Transformations.switchMap(currentCategory) { category ->
           // queries the database for a new list of items of the new category wrapped into a LiveData<Item>
           itemDao.getItemByCategoryID(category.id)
    }

    init {
        currentCategory.value = getStartCategoryFromSomewhere()
    }

    fun search(id: Int) { // is called by the fragment when you want to change the category. This can also be a search String...
        currentCategory.value?.let { current ->
            // sets a Category as the new value of the MutableLiveData
            current.value = getNewCategoryByIdFromSomeWhereElse(id)
        }
    }
}


回答4:

I implement the bar code searching product using the following approach.
Everytime the value of productBarCode changes, the product will be searched in the room db.

@AppScoped
class PosMainViewModel @Inject constructor(
var localProductRepository: LocalProductRepository) : ViewModel() {

val productBarCode: MutableLiveData<String> = MutableLiveData()

val product: LiveData<LocalProduct> = Transformations.switchMap(productBarCode) { barcode ->
    localProductRepository.getProductByBarCode(barcode)
}

init {
    productBarCode.value = ""
}

fun search(barcode: String) {
    productBarCode.value = barcode
}}

In activity

posViewModel.product.observe(this, Observer {
        if (it == null) {
           // not found
        } else {
            productList.add(it)
            rvProductList.adapter!!.notifyDataSetChanged()
        }
    })

for searching

posViewModel.search(barcode) //search param or barcode


回答5:

Here is a working example in KOTLIN

in the Fragment

binding.search.addTextChangedListener { text ->
            viewModel.searchNameChanged(text.toString())
        }


        viewModel.customers.observe(this, Observer {
            adapter.submitList(it)
            binding.swipe.isRefreshing=false
        })
  • search -> is my edit text
  • customers -> is the data list in the viewModel

View Model

     private val _searchStringLiveData = MutableLiveData<String>()

         val customers = Transformations.switchMap(_searchStringLiveData){string->
                repository.getCustomerByName(string)
            }

    init {
            refreshCustomers()
            _searchStringLiveData.value=""
        }


fun searchNameChanged(name:String){
        _searchStringLiveData.value=name
    }