NavArgument.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.navigation;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * NavArgument denotes an argument that is supported by a {@link NavDestination}.
 * <p>
 * A NavArgument has a type and optionally a default value, that are used to read/write
 * it in a Bundle. It can also be nullable if the type supports it.
 */
public final class NavArgument {
    @NonNull
    private final NavType mType;
    private final boolean mIsNullable;
    private final boolean mDefaultValuePresent;
    @Nullable
    private final Object mDefaultValue;

    NavArgument(@NonNull NavType<?> type,
            boolean isNullable,
            @Nullable Object defaultValue,
            boolean defaultValuePresent) {
        if (!type.isNullableAllowed() && isNullable) {
            throw new IllegalArgumentException(type.getName() + " does not allow nullable values");
        }

        if (!isNullable && defaultValuePresent && defaultValue == null) {
            throw new IllegalArgumentException("Argument with type " + type.getName()
                    + " has null value but is not nullable.");
        }

        this.mType = type;
        this.mIsNullable = isNullable;
        this.mDefaultValue = defaultValue;
        this.mDefaultValuePresent = defaultValuePresent;
    }

    /**
     * This method can be used to distinguish between a default value of `null` and an argument
     * without an explicit default value.
     * @return true if this argument has a default value (even if that value is set to null),
     *  false otherwise
     */
    public boolean isDefaultValuePresent() {
        return mDefaultValuePresent;
    }

    /**
     * Get the type of this NavArgument.
     * @return the NavType object denoting the type that can be help in this argument.
     */
    @NonNull
    public NavType<?> getType() {
        return mType;
    }

    /**
     * Check if this argument allows passing a `null` value.
     * @return true if `null` is allowed, false otherwise
     */
    public boolean isNullable() {
        return mIsNullable;
    }

    /**
     * Returns the default value of this argument or `null` if it doesn't have a default value.
     * Use {@link #isDefaultValuePresent()} to distinguish between `null` and absence of a value.
     * @return The deafult value assigned to this argument.
     */
    @Nullable
    public Object getDefaultValue() {
        return mDefaultValue;
    }

    @SuppressWarnings("unchecked")
    void putDefaultValue(@NonNull String name, @NonNull Bundle bundle) {
        if (mDefaultValuePresent) {
            mType.put(bundle, name, mDefaultValue);
        }
    }

    boolean verify(@NonNull String name, @NonNull Bundle bundle) {
        if (!mIsNullable && bundle.containsKey(name) && bundle.get(name) == null) {
            return false;
        }
        try {
            mType.get(bundle, name);
        } catch (ClassCastException e) {
            return false;
        }
        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        NavArgument that = (NavArgument) o;

        if (mIsNullable != that.mIsNullable) return false;
        if (mDefaultValuePresent != that.mDefaultValuePresent) return false;
        if (!mType.equals(that.mType)) return false;
        return mDefaultValue != null ? mDefaultValue.equals(that.mDefaultValue)
                : that.mDefaultValue == null;
    }

    @Override
    public int hashCode() {
        int result = mType.hashCode();
        result = 31 * result + (mIsNullable ? 1 : 0);
        result = 31 * result + (mDefaultValuePresent ? 1 : 0);
        result = 31 * result + (mDefaultValue != null ? mDefaultValue.hashCode() : 0);
        return result;
    }

    /**
     * A builder for constructing {@link NavArgument} instances.
     */
    public static final class Builder {
        @Nullable
        private NavType<?> mType;
        private boolean mIsNullable = false;
        @Nullable
        private Object mDefaultValue;
        private boolean mDefaultValuePresent = false;

        /**
         * Set the type of the argument.
         * @param type Type of the argument.
         * @return This builder.
         */
        @NonNull
        public Builder setType(@NonNull NavType<?> type) {
            mType = type;
            return this;
        }

        /**
         * Specify if the argument is nullable.
         * The NavType you set for this argument must allow nullable values.
         * @param isNullable Argument will be nullable if true.
         * @return This builder.
         * @see NavType#isNullableAllowed()
         */
        @NonNull
        public Builder setIsNullable(boolean isNullable) {
            mIsNullable = isNullable;
            return this;
        }

        /**
         * Specify the default value for an argument. Calling this at least once will cause the
         * argument to have a default value, even if it is set to null.
         * @param defaultValue Default value for this argument.
         *                     Must match NavType if it is specified.
         * @return This builder.
         */
        @NonNull
        public Builder setDefaultValue(@Nullable Object defaultValue) {
            mDefaultValue = defaultValue;
            mDefaultValuePresent = true;
            return this;
        }

        /**
         * Build the NavArgument specified by this builder.
         * If the type is not set, the builder will infer the type from the default argument value.
         * If there is no default value, the type will be unspecified.
         * @return the newly constructed NavArgument.
         */
        @NonNull
        public NavArgument build() {
            if (mType == null) {
                mType = NavType.inferFromValueType(mDefaultValue);
            }
            return new NavArgument(mType, mIsNullable, mDefaultValue, mDefaultValuePresent);
        }
    }
}