Search code examples
javaandroidandroid-listviewandroid-arrayadapter

updating listview when data is removed from the arrayadapter


Im creating a listView and setting a custom arrayAdapter to it in my main activity. Using an onClickListener within the getView method of the adapter to try and remove the item clicked. Its being removed from the array in the adapter but i cant make the listview refresh to show that using notifyDataSetChanged. Ive tried using an onItemClickListener in the main activity instead but i can only get that to work by setting the items in the object view to not clickable which wont work because of the checkboxes. ive tried calling notifyDataSetChanged in the onClickListener in getView, at the end of getView itself but neither do anything. the only way ive gotten the listview to change is creating a new instance of the adapter and setting the listview again which i do in the main activity to add new items, but i dont know if i could do that from the adapter and i cant set onClick for the listview in main activity because of above. i know the data is being removed from the array because the log lines ADAPTER ONCLICK START and END show the removal, but i cannot find a way to update the screen at that moment.

heres the main activity

public class MainActivity extends AppCompatActivity {
    CustomAdapter adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ArrayList<TaskModel> tasks = read();
        //listview
        ListView listView = findViewById(R.id.task_list);
        adapter = new CustomAdapter(this, tasks);
        listView.setAdapter(adapter);
        //add button
        Button addButton = findViewById(R.id.add_button);
        addButton.setOnClickListener(v -> {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            View mView = getLayoutInflater().inflate(R.layout.add_dialog,null);
            final EditText txt_inputText = mView.findViewById(R.id.editText);
            builder.setView(mView);
            builder.setMessage("add a new item").setPositiveButton("Add", (dialog, which) -> {
                //do the add here
                Log.d("NEW START", String.valueOf(tasks.size()));
                tasks.add(new TaskModel(txt_inputText.getText().toString(), false));
                write(tasks);
                adapter = new CustomAdapter(this, tasks);
                listView.setAdapter(adapter);
                Toast.makeText(getApplicationContext(),"new item added",Toast.LENGTH_SHORT).show();
                Log.d("NEW END", String.valueOf(tasks.size()));
            }).setNegativeButton("Cancel", (dialog, which) -> Toast.makeText(getApplicationContext(),"new item cancelled",Toast.LENGTH_SHORT).show());
            AlertDialog alert = builder.create();
            alert.show();
        });
    }
}

and the adapter

public class CustomAdapter extends ArrayAdapter{
    Context mContext;
    ArrayList<TaskModel> data;
    public CustomAdapter(Context context, ArrayList<TaskModel> data) {
        super(context, R.layout.list_item, data);
        this.mContext = context;
        this.data = data;
    }

    public static class ViewHolder{
        TextView txtName;
        CheckBox chkBox;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent){
        Log.d("ADAPTER START", String.valueOf(data.size()));
        TaskModel model = (TaskModel) getItem(position);
        ViewHolder viewHolder;
        if(convertView == null){
            viewHolder = new ViewHolder();
            LayoutInflater inflater = LayoutInflater.from(getContext());
            convertView = inflater.inflate(R.layout.list_item, parent, false);
            viewHolder.txtName = convertView.findViewById(R.id.textView);
            viewHolder.chkBox = convertView.findViewById(R.id.checkBox);
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
            viewHolder.txtName.setOnClickListener(v -> {
                Log.d("ADAPTER ONCLICK START", String.valueOf(data.size()));
                for(int i =0; i < data.size(); i++){
                    if(viewHolder.txtName.getText().equals(data.get(i).name)){
                        data.remove(i);
                    }
                }
                write(data);
                refresh();
                Log.d("ADAPTER ONCLICK END", String.valueOf(data.size()));
            });
        viewHolder.chkBox.setOnCheckedChangeListener((buttonView, isChecked)->{
            if(isChecked){
                //do checked

            }else{
                //do unchecked

            }
        });

        viewHolder.txtName.setText(model.name);
        viewHolder.chkBox.setChecked(model.done);
        return convertView;
    }
}

these are the methods for reading and writing data they use bufferedstreams to edit a json array in internalstorage

public class FileController {
public static ArrayList<TaskModel> read(){
    ArrayList<TaskModel> tasks = new ArrayList<>();
    try{
        FileInputStream inputstream = MyApplication.getAppContext().openFileInput("hello.json");
        InputStreamReader streamreader = new InputStreamReader(inputstream);
        BufferedReader bufferedReader = new BufferedReader(streamreader);
        StringBuilder b = new StringBuilder();
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            b.append(line);
        }
        JSONArray jsonArray = new JSONArray(b.toString());
        for(int i=0; i < jsonArray.length(); i++){
            tasks.add(new TaskModel(jsonArray.getJSONObject(i)));
        }
        bufferedReader.close();
    }catch (Exception e){
        e.printStackTrace();
    }
    return tasks;
}
public static void write(ArrayList<TaskModel> tasks){
    try {
        FileOutputStream outputStream = MyApplication.getAppContext().openFileOutput("hello.json", MODE_PRIVATE);
        JSONArray jsonArray = new JSONArray();
        for(int i =0; i < tasks.size(); i++){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("name", tasks.get(i).name);
            jsonObject.put("done", tasks.get(i).done);
            jsonArray.put(jsonObject);
        }
        outputStream.write(jsonArray.toString().getBytes());
        outputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}


Solution

  • Try changing the structure like this:

    • Create an event listener interface in your adapter
    • Make your activity implement that listener
    • Propagate the click or touch events to the activity through the listener in your adapter
    • Let only your activity process the data not the adapter

    Solution Code Prototype

    Making some quick changes on your code you would have your adapter class like following:

    public class CustomAdapter extends ArrayAdapter {
    
        public interface CustomAdapterEventListener {
            void onItemDelete(int index);
            void onItemCheck(int index, boolean checked);
        }
    
        Context mContext;
        ArrayList<TaskModel> data;
        CustomAdapterEventListener listener;
    
        public CustomAdapter(Context context, ArrayList<TaskModel> data, CustomAdapterEventListener listener) {
            super(context, R.layout.list_item, data);
            this.mContext = context;
            this.data = data;
            this.listener = listener;
        }
    
        public static class ViewHolder{
            TextView txtName;
            CheckBox chkBox;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            TaskModel model = (TaskModel) getItem(position);
            ViewHolder viewHolder;
            if(convertView == null) {
                viewHolder = new ViewHolder();
                LayoutInflater inflater = LayoutInflater.from(getContext());
                convertView = inflater.inflate(R.layout.list_item, parent, false);
                viewHolder.txtName = convertView.findViewById(R.id.textView);
                viewHolder.chkBox = convertView.findViewById(R.id.checkBox);
                convertView.setTag(viewHolder);
    
                // You wanna setup the listeners only once when view binding
                // Notify the listener (the activity in this case) that an item delete request has been made
                viewHolder.txtName.setOnClickListener(v -> listener.onItemDelete(position));
                // Notify the listener (the activity in this case) that item check has changed
                viewHolder.chkBox.setOnCheckedChangeListener((buttonView, isChecked)->
                    listener.onItemCheck(position, isChecked));
            }
            else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
    
            // Always execute the data filling code
            viewHolder.txtName.setText(model.name);
            viewHolder.chkBox.setChecked(model.done);
            return convertView;
        }
    
        public void setDataList(ArrayList<TaskModel> dataList) {
            this.data.clear();
            this.data.addAll(dataList); // Mind here that we don't change the list's reference
            notifyDataSetChanged();
        }
    }
    

    Then you would implenment the CustomAdapterEventListener in your activity so that you can process the data upon user interaction with the buttons or checkboxes within list items:

    public class MainActivity extends AppCompatActivity implements CustomAdapterEventListener {
    
        private final ArrayList<TaskModel> tasks = new ArrayList<>(); // Make tasks list global
        CustomAdapter adapter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tasks.addAll(read()); // Read tasks from file, repo or database etc.
            //listview
            ListView listView = findViewById(R.id.task_list);
            adapter = new CustomAdapter(this, tasks, this); // Added the listener
            listView.setAdapter(adapter); // Set adapter once during the activitiy's lifecycle
                                          // unless you need to manage multiple adapters
            //add button
            Button addButton = findViewById(R.id.add_button);
            addButton.setOnClickListener(v -> {
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                View mView = getLayoutInflater().inflate(R.layout.add_dialog,null);
                final EditText txt_inputText = mView.findViewById(R.id.editText);
                builder.setView(mView);
                builder.setMessage("add a new item").setPositiveButton("Add", (dialog, which) -> {
                    //do the add here
                    Log.d("NEW START", String.valueOf(tasks.size()));
                    tasks.add(new TaskModel(txt_inputText.getText().toString(), false));
                    write(tasks);
                    adapter.setDataList(tasks);
                    Toast.makeText(getApplicationContext(),"new item added",Toast.LENGTH_SHORT).show();
                    Log.d("NEW END", String.valueOf(tasks.size()));
                }).setNegativeButton("Cancel", (dialog, which) -> Toast.makeText(getApplicationContext(),"new item cancelled",Toast.LENGTH_SHORT).show());
                AlertDialog alert = builder.create();
                alert.show();
            });
        }
    
    
        @Override
        public void onItemDelete(int index) {
            // Delete the data item here
            if(index < tasks.size) {
                // Make sure the index valid
                tasks.remove(index);
                // Update the data in adapter
                adapter.setDataList(tasks);
                write(tasks); // Save data in file
            }
        }
    
        @Override
        public void onItemCheck(int index, boolean checked) {
            // Update the data item's done state here
            if(index < tasks.size) {
                // Make sure the index valid
                tasks.get(index).done = checked;
                // Update the data in adapter
                adapter.setDataList(tasks);
                write(tasks); // Save data in file
            }
        }
    }
    

    Give it a shot, restructure your code as in the above and see how it works.
    Note: I didn't tested the code.