Finchy70
Finchy70

Reputation: 473

Getting child from ListView causes android.view.View android.view.View.findViewById(int) on a null object reference

When the edit button is clicked on my activity_main.xml it fires this method in my main activity.

public void editActivity(View view) {
    editIds.clear();
    //Check to see if any activities are ticked (checked) and grab the id if they are.
    for (int i = 0; i < adapter.getCount(); i++) {
        CheckBox c = listView.getChildAt(i).findViewById(R.id.checkBox);

        if (c.isChecked()) {
            editIds.add(adapter.getItemId(i));
        }
    }
    //If only one item is checked start the Edit activity
    if (editIds.size() == 1) {
        Intent intent = new Intent(this, EditActivity.class);
        intent.putExtra("ID", String.valueOf(editIds.get(0)));
        startActivityForResult(intent, 2);
    }
    //If more than 1 or none are checked inform the user
    else {
        String output = "VIEW / EDIT: Please select one activity.";
        Toast.makeText(this, output, Toast.LENGTH_SHORT).show();
    }
}

This is checking for any checkboxes checked in the ListView. All works fine until the size of the ListView outgrows the size of the screen. What I mean is as soon as the list has to be scrolled clicking Edit causes the app to crash with this error:

Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:385)
        at android.view.View.performClick(View.java:5610) 
        at android.view.View$PerformClick.run(View.java:22265) 
        at android.os.Handler.handleCallback(Handler.java:751) 
        at android.os.Handler.dispatchMessage(Handler.java:95) 
        at android.os.Looper.loop(Looper.java:154) 
        at android.app.ActivityThread.main(ActivityThread.java:6077) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756) 
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.View android.view.View.findViewById(int)' on a null object reference
        at com.example.activitytracker.MainActivity.editActivity(MainActivity.java:108)

All works fine until the size of the list exceeds the size of the screen and some research has indicated that although my custom adapter contains all the entries for the entire list view the list view only stores the visible entries. I think this is why I get the error.

How can I populate the ArrayList editIds with the checked items id so I can either retrieve that record from the SQLite db or delete it.

My adapter:

package com.example.activitytracker;

import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;

public class ActivityAdapter extends CursorAdapter {
    public ActivityAdapter(Context context, Cursor cursor) {

        super(context, cursor, 0);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return LayoutInflater.from(context).inflate(R.layout.activity, parent, false);
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        // Find fields to populate in inflated template
        TextView activityTitle = (TextView) view.findViewById(R.id.activityTitle);
        TextView activityDescription = (TextView) view.findViewById(R.id.activityDescription);
        TextView activityDate = (TextView) view.findViewById(R.id.activityDate);

        // Extract properties from cursor
        String activity = cursor.getString(cursor.getColumnIndexOrThrow("activity"));
        String description = cursor.getString(cursor.getColumnIndexOrThrow("description"));
        String date = cursor.getString(cursor.getColumnIndexOrThrow("date"));


        // Shorten description for cleaner screen display
        String displayValueOfDescription = description;
        if (description.length() > 20) {
            displayValueOfDescription = description.substring(0, 19) + "...";
        }

        // Create UK date format for screen display
        String yearDB = date.substring(0, 4);
        String monthDB = date.substring(5, 7);
        String dayDB = date.substring(8, 10);
        String dateDB = dayDB + "-" + monthDB + "-" + yearDB;

        // Populate fields with extracted properties
        activityTitle.setText(activity);
        activityDescription.setText(String.valueOf(displayValueOfDescription));
        activityDate.setText(String.valueOf(dateDB));
    }

    public void update(Cursor cursor) {
        this.swapCursor(cursor);
        this.notifyDataSetChanged();
    }
}

My layout_main.xml:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:theme="@style/AppTheme">

    <include
        android:id="@+id/toolbar"
        layout="@layout/toolbar" />

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@string/activity"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textSize="18sp"
            android:textStyle="italic"
            android:padding="5dp"
            android:layout_weight="4"/>

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@string/description"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textSize="18sp"
            android:textStyle="italic"
            android:padding="5dp"
            android:layout_weight="6"/>

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@string/date"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textSize="18sp"
            android:textStyle="italic"
            android:padding="5dp"
            android:layout_weight="3"/>


    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0.1">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme">

        <Button
            android:id="@+id/edit"
            android:onClick="editActivity"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/edit" />
        <Button
            android:id="@+id/delete"
            android:onClick="deleteActivity"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/delete" />


    </LinearLayout>


</LinearLayout>

My ListView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <CheckBox
        android:id="@+id/checkBox"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@null"
        android:layout_weight="1"/>

    <TextView
        android:id="@+id/activityTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/activity"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textSize="15sp"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:paddingLeft="5dp"
        android:layout_weight="3"/>

    <TextView
        android:id="@+id/activityDescription"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/description"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textSize="15sp"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:paddingLeft="5dp"
        android:layout_weight="6"/>

    <TextView
        android:id="@+id/activityDate"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/date"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textSize="15sp"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:paddingLeft="5dp"
        android:layout_weight="3"/>
</LinearLayout>

Any help is appreciated as I am a complete beginner with Android and Java.

Here is my onCreate()

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        dbManager = new DBManager(this);
        dbManager.open();
        adapter = new ActivityAdapter(this, dbManager.fetch());
        listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(adapter);
    }

EDIT

I have added this code to my Adapter

 checked.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                // Do the stuff you want for the case when the row TextView is clicked

                // you may want to set as the tag for the TextView the position paremeter of the `getView` method and then retrieve it here
                Integer realPosition = (Integer) v.getTag();
                // using realPosition , now you know the row where this TextView was clicked
                Log.d("CLICKEDCHECK", "POS=" + v.getId());
            }
        });

Im logging the getId but this is the same no matter what tickbox i check. How do I get to the records id for that particular checkbox? Then I could add or remove them from an array list.

EDIT Update with code from devgianlu.

Code always returns the same id whatever box you check to edit or delete.

here is the updated ActivityAdapter

package com.example.activitytracker;

import android.content.Context;
import android.database.Cursor;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CursorAdapter;
import android.widget.TextView;

public class ActivityAdapter extends CursorAdapter {
    private final SparseBooleanArray checked = new SparseBooleanArray();


    public ActivityAdapter(Context context, Cursor cursor) {
        super(context, cursor, 0);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return LayoutInflater.from(context).inflate(R.layout.activity, parent, false);
    }

    @Override
    public void bindView(View view, Context context, final Cursor cursor) {
        // Find fields to populate in inflated template
        TextView activityTitle = (TextView) view.findViewById(R.id.activityTitle);
        TextView activityDescription = (TextView) view.findViewById(R.id.activityDescription);
        TextView activityDate = (TextView) view.findViewById(R.id.activityDate);
        CheckBox check = (CheckBox) view.findViewById(R.id.checkBox);
        check.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                checked.put(cursor.getPosition(), isChecked);
            }
        });

        // Extract properties from cursor
        String activity = cursor.getString(cursor.getColumnIndexOrThrow("activity"));
        String description = cursor.getString(cursor.getColumnIndexOrThrow("description"));
        String date = cursor.getString(cursor.getColumnIndexOrThrow("date"));


        // Shorten description for cleaner screen display
        String displayValueOfDescription = description;
        if (description.length() > 20) {
            displayValueOfDescription = description.substring(0, 19) + "...";
        }

        // Create UK date format for screen display
        String yearDB = date.substring(0, 4);
        String monthDB = date.substring(5, 7);
        String dayDB = date.substring(8, 10);
        String dateDB = dayDB + "-" + monthDB + "-" + yearDB;

        // Populate fields with extracted properties
        activityTitle.setText(activity);
        activityDescription.setText(String.valueOf(displayValueOfDescription));
        activityDate.setText(String.valueOf(dateDB));
    }

    public boolean isChecked(int pos) {
        return checked.get(pos, false);
    }

    public void update(Cursor cursor) {
        this.swapCursor(cursor);
        this.notifyDataSetChanged();
    }
}

and my call to the isChecked method in MainActivity

public void editActivity(View view) {
        editIds.clear();
        //Check to see if any activities are ticked (checked) and grab the id if they are.
        // Add checked items to deleteIds
        for (int i = 0; i < adapter.getCount(); i++) {
            if (adapter.isChecked(i)) {
                editIds.add(adapter.getItemId(i));
                Log.d("EDITACT", "ID="+adapter.getItemId(i) + "size="+editIds.size());
            }
        }
        //If only one item is checked start the Edit activity
        if (editIds.size() == 1) {
            Intent intent = new Intent(this, EditActivity.class);
            intent.putExtra("ID", String.valueOf(editIds.get(0)));
            startActivityForResult(intent, 2);
        }
        //If more than 1 or none are checked inform the user
        else {
            String output = "VIEW / EDIT: Please select one activity.";
            Toast.makeText(this, output, Toast.LENGTH_SHORT).show();
        }
    }


Further update

My ActivityAdapter now looks like this.

package com.example.activitytracker;

import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;

import java.util.ArrayList;
import java.util.List;

class Activities {
    private boolean checked = false;
    private DBManager dbManager;

    Cursor cursor = dbManager.fetch();
    public Activities(Cursor cursor) {
        List<Activities> items = new ArrayList<>();
        while (cursor.moveToNext()){
            items.add(new Activities(cursor));
        }
    }
}

public class ActivityAdapter extends BaseAdapter {
    private final List<Activities> items;
    private final LayoutInflater inflater;

    public ActivityAdapter(Context context, List<Activities> items) {
        this.items = items;
        this.inflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.activity, parent, false);
        }

        final Activities item = items.get(position);
        CheckBox checkBox = convertView.findViewById(R.id.checkBox);
        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
//                item.isChecked() = isChecked;
            }
        });

        // *** Bind the data to the view ***

        return convertView;
    }

    public boolean isChecked(int pos) {
//        return items.get(pos).checked;
        return true;
    }

    @Override
    public int getCount() {
        return items.size();
    }

    @Override
    public Object getItem(int position) {
        return items.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
}

Also now my adapter is not populating my listView on startup and any call to adapter crashes the app.

Here is where I used to check the listview for checked boxes then refresh the adaptor. Not sure what to do here now.

        deleteIds.clear();
        // Add checked items to deleteIds
        for (int i = 0; i < adapter.getCount(); i++) {
            CheckBox c = listView.getChildAt(i).findViewById(R.id.checkBox);
            if (c.isChecked()) {
                deleteIds.add(adapter.getItemId(i));
            }
        }
        //Check to see if any activities are ticked (checked)
        if (deleteIds.size() > 0) {
            //If there are checked activities a dialog is displayed for user to confirm
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("Are you sure you want to delete?")
                    .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            for (int i = 0; i < deleteIds.size(); i++) {
                                dbManager.delete(deleteIds.get(i));
                                adapter.update(dbManager.fetch());
                            }
                        }
                    })
                    .setNegativeButton("No", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            deleteIds.clear();
                            dialog.cancel();
                        }
                    }).show();
        }
    }

Thanks so much for you help so far.

Upvotes: 0

Views: 84

Answers (1)

devgianlu
devgianlu

Reputation: 1580

This is a bit of code I wrote to put all the Cursor data into objects and then bind these objects to views. This way it is much easier to control the checkbox state.

public class CustomItem {
    public boolean checked = false;

    public CustomItem(Cursor cursor) {
        // Get yuor data and put it in the object
    }
}

public class CustomAdapter extends BaseAdapter {
    private final List<CustomItem> items;
    private final LayoutInflater inflater;

    public CustomAdapter(Context context, List<CustomItem> items) {
        this.items = items;
        this.inflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = inflater.inflate(/* Your layout ID */, parent, false);
        }

        final CustomItem item = items.get(position);
        CheckBox checkBox = convertView.findViewById(R.id.checkBox);
        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                item.checked = isChecked;
            }
        });

        // *** Bind the data to the view ***

        return convertView;
    }

    public boolean isChecked(int pos) {
        return items.get(pos).checked;
    }

    @Override
    public int getCount() {
        return items.size(); 
    }

    @Override
    public Object getItem(int position) {
        return items.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
}


Cursor cursor = ....;

List<CustomItem> items = new ArrayList<>();
while (cursor.moveToNext())
    items.add(new CustomItem(cursor));

Adapter adapter = new BaseAdapter(this /* Your activity */, items);
listView.setAdapter(adapter);

for (int i = 0; i < adapter.getCount(); i++) {
    System.out.println(i + " IS " + adapter.isChecked(i));
}

Upvotes: 1

Related Questions