NestedGraphに値を渡す方法は2パターンあるらしい。。
NavigationComponentを使いはじめの頃、
画面遷移図がややこしくなってきた時にレイアウトxmlと同様にincludeできることを知って、
startDestinationに指定されたfragmentタグの中にargmentを置いてみて動かなかったことがあると思いますが、
[参考リンク] によると別ファイルか同一ファイル内かで書き方が別れてしまうらしい。。。
なぜこんなにも覚えづらい実装なんだろう。。
脳死で覚えるのもいいのですが、複雑な画面遷移ロジックのタスクが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();
}
}
この中の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;
}
ネストを処理している部分の解説
こちらがネストをハンドリングしている部分の実態になるのですが、
} 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);
}
このコードブロックに出てくる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();
}
こちらに飛ぶと、何やら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が起きるから仕方がないのかなと思いました。
以上になります。