LoginSignup
3
1

More than 3 years have passed since last update.

Android Navigation ComponentのNested Graphのargumentをxmlの1階層目に置く理由を調べてみた

Posted at

NestedGraphに値を渡す方法は2パターンあるらしい。。

NavigationComponentを使いはじめの頃、
画面遷移図がややこしくなってきた時にレイアウトxmlと同様にincludeできることを知って、
startDestinationに指定されたfragmentタグの中にargmentを置いてみて動かなかったことがあると思いますが、
[参考リンク] によると別ファイルか同一ファイル内かで書き方が別れてしまうらしい。。。

なぜこんなにも覚えづらい実装なんだろう。。 :thinking:

脳死で覚えるのもいいのですが、複雑な画面遷移ロジックのタスクが2〜3ヶ月来ない間に忘れてしまいそうなので、
何故こんなことになってしまったのかをきちんと内部実装から確認してみることにしました。

まずxmlからインスタンスに変換している部分を探してみる。。。

レイアウトxmlを使う時には皆さまお馴染みのinflate関数がありますが、
NavigationComponentのxmlをインスタンス化する時には内部的にinflateという名前の関数を使っているようです。

/**
 * Inflate a NavGraph from the given XML resource id.
 *
 * @param graphResId
 * @return
 */
@SuppressLint("ResourceType")
public NavGraph inflate(@NavigationRes int graphResId) {
    Resources res = mContext.getResources();
    XmlResourceParser parser = res.getXml(graphResId);
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    try {
        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            // Empty loop
        }
        if (type != XmlPullParser.START_TAG) {
            throw new XmlPullParserException("No start tag found");
        }
        String rootElement = parser.getName();
        NavDestination destination = inflate(res, parser, attrs);
        if (!(destination instanceof NavGraph)) {
            throw new IllegalArgumentException("Root element <" + rootElement + ">"
                    + " did not inflate into a NavGraph");
        }
        return (NavGraph) destination;
    } catch (Exception e) {
        throw new RuntimeException("Exception inflating "
                + res.getResourceName(graphResId) + " line "
                + parser.getLineNumber(), e);
    } finally {
        parser.close();
    }
}

NavInflater

この中のNavDestination destination = inflate(res, parser, attrs);部分を見てみると、
下記のようにxmlのタグごとに処理を振り分ける処理になっており、ここを見れば原因が特定できそうです。

private NavDestination inflate(Resources res, XmlResourceParser parser, AttributeSet attrs)
        throws XmlPullParserException, IOException {
    Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
    final NavDestination dest = navigator.createDestination();
    dest.onInflate(mContext, attrs);
    final int innerDepth = parser.getDepth() + 1;
    int type;
    int depth;
    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && ((depth = parser.getDepth()) >= innerDepth
            || type != XmlPullParser.END_TAG)) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        if (depth > innerDepth) {
            continue;
        }
        final String name = parser.getName();
        if (TAG_ARGUMENT.equals(name)) {
            inflateArgument(res, dest, attrs);
        } else if (TAG_DEEP_LINK.equals(name)) {
            inflateDeepLink(res, dest, attrs);
        } else if (TAG_ACTION.equals(name)) {
            inflateAction(res, dest, attrs);
        } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
            final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
            final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
            ((NavGraph) dest).addDestination(inflate(id));
            a.recycle();
        } else if (dest instanceof NavGraph) {
            ((NavGraph) dest).addDestination(inflate(res, parser, attrs));
        }
    }
    return dest;
}

NavInflater

ネストを処理している部分の解説

こちらがネストをハンドリングしている部分の実態になるのですが、

            } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
                final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
                final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
                ((NavGraph) dest).addDestination(inflate(id));
                a.recycle();
            } else if (dest instanceof NavGraph) {
                ((NavGraph) dest).addDestination(inflate(res, parser, attrs));
            }

1つ目のelse ifはTAG_INCLUDEとある通り他のファイルをincludeした時の分岐で、
2つ目のelse ifはdest instanceof NavGraphとある通り<navigation>タグでさらにnavGraphになっている時の処理です。
1つ目の分岐でも2つ目の分岐でもinflateされたものをaddDestinationしています。
ちなみに1つ目のinflateは当記事1つ目のコードブロックの関数へ、
2つ目は2つ目のコードブロックの関数にそれぞれ再帰的にinflateし続けています。

そしてaddDestination関数のコードがこちらです。

public final void addDestination(@NonNull NavDestination node) {
    if (node.getId() == 0) {
        throw new IllegalArgumentException("Destinations must have an id."
                + " Call setId() or include an android:id in your navigation XML.");
    }
    NavDestination existingDestination = mNodes.get(node.getId());
    if (existingDestination == node) {
        return;
    }
    if (node.getParent() != null) {
        throw new IllegalStateException("Destination already has a parent set."
                + " Call NavGraph.remove() to remove the previous parent.");
    }
    if (existingDestination != null) {
        existingDestination.setParent(null);
    }
    node.setParent(this);
    mNodes.put(node.getId(), node);
}

NavGraph

このコードブロックに出てくるmNodesというのは
final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
という型のプロパティで、見慣れない型ですがkeyがIntegerでvalueがNavDestinationのHashMapになっています。

それで、addDestination関数がのException以外の部分を抜き出してみると、

node.setParent(this);
mNodes.put(node.getId(), node);

となっていて、ネストをハンドリングしていた((NavGraph) dest).addDestinationのdestを自分の親に登録し、
ナビゲーショングラフxmlのidで整理されたNavDestinationのHashMapに自分を登録する処理となっています。
この時登録しているインスタンスがNavGraphとNavDestinationと別インスタンスになっていて、
NavGraphはNavDestinationと配列のインターフェースを継承したクラスになっています。

ここで、先ほどのXMLのタグごとに処理を切り分けた場所のどの分岐で<argument>タグが処理されているか見てみると、

        if (TAG_ARGUMENT.equals(name)) {
            inflateArgument(res, dest, attrs);
        }

こちらから、

private void inflateArgument(Resources res, NavDestination dest, AttributeSet attrs)
        throws XmlPullParserException {
    final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavArgument);
    String name = a.getString(R.styleable.NavArgument_android_name);
    TypedValue value = sTmpValue.get();
    if (value == null) {
        value = new TypedValue();
        sTmpValue.set(value);
    }
    if (a.getValue(R.styleable.NavArgument_android_defaultValue, value)) {
        switch (value.type) {
            case TypedValue.TYPE_STRING:
                dest.getDefaultArguments().putString(name, value.string.toString());
                break;
            case TypedValue.TYPE_DIMENSION:
                dest.getDefaultArguments().putInt(name,
                        (int) value.getDimension(res.getDisplayMetrics()));
                break;
            case TypedValue.TYPE_FLOAT:
                dest.getDefaultArguments().putFloat(name, value.getFloat());
                break;
            case TypedValue.TYPE_REFERENCE:
                dest.getDefaultArguments().putInt(name, value.data);
                break;
            default:
                if (value.type >= TypedValue.TYPE_FIRST_INT
                        && value.type <= TypedValue.TYPE_LAST_INT) {
                    dest.getDefaultArguments().putInt(name, value.data);
                } else {
                    throw new XmlPullParserException("unsupported argument type " + value.type);
                }
        }
    }
    a.recycle();
}

NavInflater

こちらに飛ぶと、何やらswitch文で型ごとにdest.getDefaultArguments().put~~~~としていますね。

この中では下記のように

private Bundle mDefaultArguments;

省略

/**
 * Gets the argument bundle to be used by default when navigating to this action.
 * @return bundle of default argument values
 */
@Nullable
public Bundle getDefaultArguments() {
    return mDefaultArguments;
}

のようにBundle型のインスタンスを取ってきてデータを追加するという昔ながらの画面遷移時にやっていた処理をしているようです。

となると、根本原因はXMLを再帰的にパースしていく過程で<include>タグを区切りに違うインスタンスのBundleに <arguments>を保存しているから。
なぜかと言えば、推測ですがBundleに入れるデータが大きくなりすぎるとTransactionTooLargeExceptionが起きるから仕方がないのかなと思いました。

以上になります。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1