TimePicker Challenge Solution (and questions)


#1

I’ve got a working solution to the challenge to include a TimePicker dialog, though I have a few questions about my solution.
My complete source can be found here https://github.com/Bradleycorn/bnr-android/tree/ch12-challenge1

First, I created a layout for the time picker …
dialog_time.xml

<TimePicker xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/dialog_time_time_picker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

</TimePicker>

Next, since the code for displaying a dialog with a datepicker vs a time picker is nearly identical, I abstracted most of the code from the DatePickerFragment.java into an abstract class that both the date picker and time picker dialogs can use. The abstract class creates two abstract method stubs, initLayout() which child classes use to inflate their layout and set values on their views (ie. inflate the dialog_date or dialog_time layout, and set the datepicker or timepicker view’s value to the current date or time of the crime).

My First Question is, did I handle the abstraction in the best manner, or could it be improved upon? Specifically, I’m not sure I handled the abstraction of the “newInstance()” methods very well. The method is the exact same for both Date and Time picker fragments, with the only difference being the type of object that is created (new DatePickerFragment vs new TimePickerFragment). I ended up creating a static method in the parent class, getArgs(), that creates and returns the arguments bundle. And then each of the child classes has a static newInstance() method that calls getArgs, creates a new instance of the class, and attaches the arguments bundle. It seems decent enough, but I can’t help feel that there might be a way to make it better.

PickerDialogFragment.java

public abstract class PickerDialogFragment extends DialogFragment {
    private static final String ARG_DATE = "date";
    public static final String EXTRA_DATE = "com.bignerdranch.android.criminalintent.date";

    protected Calendar mCalendar;

    protected abstract View initLayout();
    protected abstract Date getDate();

    protected static Bundle getArgs(Date date) {
        Bundle args = new Bundle();
        args.putSerializable(ARG_DATE, date);
        return args;
    }


    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Date date = (Date) getArguments().getSerializable(ARG_DATE);
        mCalendar = Calendar.getInstance();
        mCalendar.setTime(date);

        View v = initLayout();

        return new AlertDialog.Builder(getActivity())
                .setTitle(R.string.date_picker_title)
                .setView(v)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Date date = getDate();
                        sendResult(Activity.RESULT_OK, date);
                    }
                })
                .create();
    }

    private void sendResult(int resultCode, Date date) {
        if (getTargetFragment() == null)
            return;

        Intent intent = new Intent();
        intent.putExtra(EXTRA_DATE, date);

        getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, intent);
    }
}

Next, I edited DatePickerFragment.java to extend my abstract base class, and created a new TimePickerFragment.java which also uses the abstract base class.

My second question is, Is there a better way to do all the work with dates, and making sure that the timepicker and datepicker only update their appropriate part of the crime’s date timestamp? I considered adding additional methods to the Crime object to allow setting the Crimes Day or Time specifically, and I considered some solutions that involved additional methods in the abstract parent dialog class to handle creating a composited and updated date, but neither “felt” right. I’m not real familiar with date handling in Java, so I’m not sure if there is a better way than what I have done here.

DatePickerFragment.java

public class DatePickerFragment extends PickerDialogFragment {
    private DatePicker mDatePicker;

    public static DatePickerFragment newInstance(Date date) {
        Bundle args = getArgs(date);
        DatePickerFragment fragment = new DatePickerFragment();
        fragment.setArguments(args);
        return fragment;
    }

    protected View initLayout() {
        View v  = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_date, null);
        mDatePicker = (DatePicker) v.findViewById(R.id.dialog_date_date_picker);
        mDatePicker.init(mCalendar.get(Calendar.YEAR), mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH), null);
        return v;
    }

    protected Date getDate() {
        //Get the date from the DatePicker
        int year = mDatePicker.getYear();
        int month = mDatePicker.getMonth();
        int day = mDatePicker.getDayOfMonth();

        //The time remains the same, so pull it from the calendar
        int hour = mCalendar.get(Calendar.HOUR_OF_DAY);
        int minute = mCalendar.get(Calendar.MINUTE);

        return new GregorianCalendar(year, month, day, hour, minute).getTime();
    }
}

TimePickerFragment.java

public class TimePickerFragment extends PickerDialogFragment {

    private TimePicker mTimePicker;

    public static TimePickerFragment newInstance(Date date) {
        Bundle args = getArgs(date);
        TimePickerFragment fragment = new TimePickerFragment();
        fragment.setArguments(args);
        return fragment;
    }


    protected View initLayout() {
        View v = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_time, null);

        mTimePicker = (TimePicker) v.findViewById(R.id.dialog_time_time_picker);
        mTimePicker.setIs24HourView(false);
        mTimePicker.setCurrentHour(mCalendar.get(Calendar.HOUR_OF_DAY));
        mTimePicker.setCurrentMinute(mCalendar.get(Calendar.MINUTE));

        return v;
    }

    protected Date getDate() {
        //TimePicker only sets the time. The date remains the same
        int year = mCalendar.get(Calendar.YEAR);
        int month = mCalendar.get(Calendar.MONTH);
        int day = mCalendar.get(Calendar.DAY_OF_MONTH);

        //Get the time from the timepicker
        int hour = mTimePicker.getCurrentHour();
        int minute = mTimePicker.getCurrentMinute();

        return new GregorianCalendar(year, month, day, hour, minute).getTime();
    }
}

Next, I added a new button to the crime fragment (only the portrait version shown here, but don’t forget to update the landscape layout as well)
fragment_crime.xml

<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="match_parent"
    android:orientation="vertical"
    tools:context="com.bignerdranch.android.criminalintent.CrimeFragment">

   ....

   <Button
        android:id="@+id/crime_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"/>

    <Button
        android:id="@+id/crime_time"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"/>

 ...

</LinearLayout>

And finally, updated the CrimeFragment.java to only show the date on the date button, and show the time on the time button, and handle clicks on the time button.
CrimeFragment.java

public class CrimeFragment extends Fragment {
    private static final String ARG_CRIME_ID = "crime_id";
    private static final String DIALOG_DATE = "DialogDate";
    private static final String DIALOG_TIME = "DialogTime";
    private static final int REQUEST_DATE = 0;
    private static final int REQUEST_TIME = 1;

    private Crime mCrime;
    private EditText mTitleField;
    private Button mDateButton;
    private Button mTimeButton;
    private CheckBox mSolvedCheckBox;

    ....

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View v = inflater.inflate(R.layout.fragment_crime, container, false);

        ...

        mDateButton = (Button) v.findViewById(R.id.crime_date);
        updateDate();
        mDateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
                FragmentManager manager = getFragmentManager();
                dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
                dialog.show(manager, DIALOG_DATE);
            }
        });

        mTimeButton = (Button) v.findViewById(R.id.crime_time);
        updateTime();
        mTimeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TimePickerFragment dialog = TimePickerFragment.newInstance(mCrime.getDate());
                FragmentManager manager = getFragmentManager();
                dialog.setTargetFragment(CrimeFragment.this, REQUEST_TIME);
                dialog.show(manager, DIALOG_TIME);
            }
        });

        ....

        return v;
    }

    private void updateDate() {
        mDateButton.setText(DateFormat.format("EEEE, MMMM d, yyyyy", mCrime.getDate()));
    }

    private void updateTime() {
        mTimeButton.setText(DateFormat.format("h:mm a", mCrime.getDate()));
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != Activity.RESULT_OK)
            return;

        Date date = (Date) data.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
        mCrime.setDate(date);

        switch (requestCode) {
            case REQUEST_DATE:
                updateDate();
                break;
            case REQUEST_TIME:
                updateTime();
                break;
        }
    }
}

That’s it! It all works. I do have the few questions above about the abstract class and the dates. But other than that, it works well.


#2

Nice work! Your solution looks great.

I like the abstract class. There’s not much you can do with that “newInstance” method. Since it’s a static method, you can’t override it.

As for the date/time, it’s just going to be a pain. One alternative that is pretty popular is the JodaTime library (https://github.com/dlew/joda-time-android). I don’t know if it helps with this situation in particular but it is very popular and simplifies a lot of the pain points with dates in java.


#3

This solution looks great, however I’m having much difficulty implementing it. I’ve gone through my code line by line and everything is performing the identical procedures. When I run my code the app crashes when clicking a crime… so the new view is having issues inflating. Looking at the consol I see a null pointer exception related to the mTimeButton and setText:

java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setText(java.lang.CharSequence)' on a null object reference at com.bignerdranch.android.criminalintent.CrimeFragment.updateTime(...) at com.bignerdranch.android.criminalintent.CrimeFragment.onCreateView(...)

Presumably this is due to the use of depreciated classes/methods used in TimePickerFragment (specifically setCurrentHour() and getCurrentHour() as well as setCurrentMinute() and getCurrentMinute())

These four methods have strikethrough in my code… when I cloned your github, the project runs great and those classes don’t show as depreciated… what am I missing here? :frowning:

(checked the min SDK versions - that doesn’t seem to be the issue… even copied all your java classes over into my project… is it some other structure setting I am missing?)

EDIT: Ok, So instead of swapping out your java class files into my project, I swapped out mine into your project… and it works! So my guess is there must be a gradle setting or something I missed that was making those depreciated methods return null values or or something. If anyone has any insight or explanation please enlighten me. Thanks!


#4

@kymatica: That null pointer exception is saying that you tried to call setText on a null object. So, your button was null.

You probably had a findViewById call somewhere where you were finding a button in your layout file. I bet the button wasn’t in your layout file, so findViewById returned null.


#5

thanks,that is helpful。


#6

I finished the whole app and then went back to implement the timepicker, and when I tried the solution above, I’m getting this error whenever I pick a time.

07-26 13:26:14.982 13646-13646/criminalintent.android.bignerdranch.com.criminalintent E/AndroidRuntime: FATAL EXCEPTION: main
Process: criminalintent.android.bignerdranch.com.criminalintent, PID: 13646
java.lang.NullPointerException: uri
at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:60)
at android.content.ContentResolver.query(ContentResolver.java:487)
at android.content.ContentResolver.query(ContentResolver.java:447)
at criminalintent.android.bignerdranch.com.criminalintent.CrimeFragment.onActivityResult(CrimeFragment.java:228)
at criminalintent.android.bignerdranch.com.criminalintent.PickerDialogFragment.sendResult(PickerDialogFragment.java:59)
at criminalintent.android.bignerdranch.com.criminalintent.PickerDialogFragment.access$000(PickerDialogFragment.java:15)
at criminalintent.android.bignerdranch.com.criminalintent.PickerDialogFragment$1.onClick(PickerDialogFragment.java:46)
at android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:168)
at android.app.ActivityThread.main(ActivityThread.java:5845)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:797)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:687)


#7

Hey, I have got same problems with using setCurrentHour() and setCurrentMinute().
And I find when I change the compileSdkVersion from 23 to 22 in build.gradle, they show well.
Or you can follow this:https://forums.bignerdranch.com/t/timepicker-setcurrenthour-and-setcurrentminute-deprecated/8472/2


#8

Please check your code at this part.


#9

@Bradleycorn I’ve tried your solution the app launches fine. But i can’t set the time of the crime to a specific time; i.e. on returning to the crime detail view from time picker the time button only displays the current time. Could this be due to the deprecated methods. Please shed some insight or your thoughts.


#10

Ooops!!! I forgot to set the target fragment. Your solution works great.:grin:


#11

asphaltnitromodapk.com new full version of Asphalt Nitro