Kotlin在DialogFragment中的合成和自定义布局

30

假设我有这个布局:

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

<ImageButton
    android:id="@+id/add_dep_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_marginEnd="5dp"
    android:layout_marginRight="5dp"
    android:src="@android:drawable/ic_input_add" />

<EditText
    android:id="@+id/add_dep_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBottom="@id/add_dep_btn"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:layout_alignTop="@id/add_dep_btn"
    android:layout_marginLeft="5dp"
    android:layout_marginStart="5dp"
    android:layout_toLeftOf="@id/add_dep_btn"
    android:layout_toStartOf="@id/add_dep_btn" />

<android.support.v7.widget.RecyclerView
    android:id="@+id/dep_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/add_dep_btn" />

<TextView
    android:id="@+id/empty_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/add_dep_text"
    android:layout_margin="20dp"
    android:gravity="center"
    android:text="@string/no_dep"
    android:textSize="22sp" />
</RelativeLayout>

我在一个DialogFragment中使用它:

class DepartmentChoiceDialog : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        builder.setTitle(R.string.choose_or_create_dep)
            .setView(R.layout.department_chooser_dialog)
            .setNegativeButton(android.R.string.cancel, { d, i ->
                d.cancel()
            })
        return builder.create()
    }
}

如果我使用合成方式引用小部件:

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    dep_list.layoutManager = LinearLayoutManager(activity)
    dep_list.itemAnimator = DefaultItemAnimator()
    dep_list.setHasFixedSize(true)
}

运行时出现以下错误:

java.lang.NullPointerException: 尝试在空对象引用上调用虚拟方法 'android.view.View android.view.View.findViewById(int)' at MyDialog._$_findCachedViewById(DepartmentChoiceDialog.kt:0)

我不明白如何在DialogFragment中使用synthetic。在Fragment和Activity中它都正常工作。


这个标题是否应该更清晰地表明它是关于Android的,并且也要说明实际问题是什么(目前只有主题,但标题中没有问题)。 - Jayson Minard
1
对于您的异常,拥有一个堆栈跟踪并注意该跟踪与您提供的代码相交点总是非常有用的。 - Jayson Minard
10个回答

14

我找到了一种适用于自定义对话框的方法。

class ServerPickerDialogFragment: AppCompatDialogFragment() 
{
  // Save your custom view at the class level
  lateinit var customView: View;
  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                            savedInstanceState: Bundle?): View? 
  {
       // Simply return the already inflated custom view
       return customView
  }

  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
      // Inflate your view here
      customView = context!!.layoutInflater.inflate(R.layout.dialog_server_picker, null) 
      // Create Alert Dialog with your custom view
      return AlertDialog.Builder(context!!)
             .setTitle(R.string.server_picker_dialog_title)
             .setView(customView)
             .setNegativeButton(android.R.string.cancel, null)
             .create()
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
  {
    super.onViewCreated(view, savedInstanceState)
    // Perform remaining operations here. No null issues.
    rbgSelectType.setOnCheckedChangeListener({ _, checkedId ->
      if(checkedId == R.id.rbSelectFromList) {
             // XYZ
      } else {
             // ABC
      }
    })
  }
}

这个解决方案有泄漏问题,例如在旋转时,因此最好在onDestroyView中添加customView = null。 - Penzzz
如果你同时重写了 onCreateDialogonCreateView,那么 DialogFragment 可能会崩溃并显示 AndroidRuntimeException: requestFeature() must be called before adding content。这至少在 API 23 上发生过,但也可能适用于其他一些 API 和情况。请参见 https://dev59.com/-WMm5IYBdhLWcg3wm_7z#21734372。更安全的解决方案是仅重写 onCreateDialog,将视图设置为对话框并将其存储在 DialogFragment 中,然后在视图上调用合成特性:this.dialogView.my_text_view.text = "..." - Mr. Goldberg

10

看起来目前默认不支持这个,但我找到了最简单的方法,就是这样做。在一个基础对话框类中:

protected abstract val containerView: View

override fun getView() = containerView
在一个子类中:
override val containerView by unsafeLazy {
    View.inflate(context, R.layout.dialog_team_details, null) as ViewGroup
}

然后您可以像平常一样使用合成视图,并将containerView用作对话框的视图。


最干净的解决方案。只有一个小问题。当您保存对视图的引用时,这种设置是否会导致内存泄漏,或者Android会处理此问题? - Bohsen
3
我使用了Leak Canary并没有注意到任何问题。另外,仔细想想,因为视图与上下文类的实例相关联,并且在配置更改时会随之消失,所以出现内存泄漏是没有道理的。 - SUPERCILEX
“因其创意独特而获得点赞。” 它确实有效。但在我的情况下(DialogFragment中的通用对话框),我遭受了一个奇怪的副作用:某种未指定的主题被应用于视图,我看到的是白色背景上的白色文本! - Bad Loser

6
之前的回答不适用,因为当你使用onCreateDialog时,不会调用onViewCreated。你应该首先导入kotlinx...department_chooser_dialog.view.dep_list,然后按以下方式使用它:
import kotlinx.android.synthetic.main.department_chooser_dialog.view.dep_list
...
class DepartmentChoiceDialog : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        val dialog = inflater.inflate(R.layout.department_chooser_dialog, null)
        dialog.dep_list.layoutManager = LinearLayoutManager(activity)
        dialog.dep_list.itemAnimator = DefaultItemAnimator()
        dialog.dep_list.setHasFixedSize(true)
        builder.setTitle(R.string.choose_or_create_dep)
               .setView(dialog)
                    ...

5

我不确定是否已经解决了这个问题……我刚刚遇到了它。如果你有一个自定义的对话框视图,可以创建一个继承自DialogFragment的类,并使用“dialog”对象导入布局中的视图。在撰写本文时,我使用的是 Android Studio 3.1.3 和 Kotlin版本1.2.41。

import kotlinx.android.synthetic.main.your_custom_layout.*

class SelectCountryBottomSheet : BottomSheetDialogFragment() {

  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
      val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
      dialog.setContentView(R.layout.your_custom_layout)
      dialog.some_custom_close_button.setOnClickListener { dismiss() }
      return dialog
  }
}

4

更改为 onCreateView 的实现方式

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.department_chooser_dialog, container, false)
}

department_chooser_dialog中使用自定义标题(TextView)和取消按钮(Button),在onActivityCreated方法中实现即可。该方法会在onCreateView方法之后运行,非常适合此需求。
override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    dep_list.layoutManager = LinearLayoutManager(activity)
    dep_list.itemAnimator = DefaultItemAnimator()
    dep_list.setHasFixedSize(true)
}

2
因为默认视图的值来自于片段(Kotlin 生成的方法 _$_findCachedViewById),但如果我们从对话框创建视图,会导致片段视图为空,因此我们不能直接使用默认的 xxx,但可以使用对话框.xxx 替换默认的 xxx。

1

块引用

在一个类似这样的 Fragment 中的 Kotlin 代码:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.your_layout, container, false).apply {
        mContentView = this
        button1.setOnClickListener { 
            //do something
        }
    }
}

反编译字节码后,您可以查看合成属性的实现:

((Button)this._$_findCachedViewById(id.button1))

而且 _$_findCachedViewById 方法:
public View _$_findCachedViewById(int var1) {
  if (this._$_findViewCache == null) {
     this._$_findViewCache = new HashMap();
  }

  View var2 = (View)this._$_findViewCache.get(var1);
  if (var2 == null) {
     View var10000 = this.getView();
     if (var10000 == null) {
        return null;
     }

     var2 = var10000.findViewById(var1);
     this._$_findViewCache.put(var1, var2);
  }

  return var2;

}

所以魔法就是 this.getView()。在 Fragment.onCreateView(inflater, container, savedInstanceState) 方法执行后,Fragment.mView 属性被赋值。如果在 onCreateView() 方法中使用 Kotlin Synthetic Properties,则会导致 NPE。以下是来自 FragmentManager.moveToState() 的代码:
case Fragment.CREATED:
    ...
    f.mView = f.performCreateView(f.performGetLayoutInflater(
                                f.mSavedFragmentState), container, 
    f.mSavedFragmentState);
    ...

为了解决 NPE,确保 getView 方法返回一个非空视图。
private var mContentView: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.your_layout, container, false).apply {
        mContentView = this
    }
}
override fun getView(): View? {
    return mContentView
}

onDestroyView()生命周期回调中,将mContentView设置为null。
override fun onDestroyView() {
    super.onDestroyView()
    mContentView = null
}

1

setContentView在OnActivityCreated调用中。因此,需要在此处调用一组合成控件来监视事件:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.layout_email_ga_code, container)
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
    dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE)
    super.onActivityCreated(savedInstanceState)
    dialog?.window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)

    btn_back?.setOnClickListener {
        mOnClickListener?.onClickCancel()
        dismiss()
    }
}

它运作正常。


0
将您的代码从onActivityCreated方法移动到onViewCreated方法中。 像这样:
import kotlinx.android.synthetic.main.department_chooser_dialog.dep_list

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    dep_list.apply {
        layoutManager = LinearLayoutManager(activity)
        itemAnimator = DefaultItemAnimator()
        setHasFixedSize(true)
    }
}

我实际上没有深入研究生成的代码,也许存在漏洞。


4
在我的情况下,onViewCreated根本没有被调用,所以这并没有起到帮助作用。 - arekolek
3
很遗憾,在DialogFragment上,onViewCreated方法不会被调用。 - Julian Suarez
实际上,@JulianSuarez在AppCompatDialogFragment上确实被调用了。 - Maks

-1

通过在onCreateDialog中填充的视图,可以访问视图。因此,如果您将视图保存在变量(rootView)中,则可以从YourDialogFragment内的任何方法访问视图。

// ...
import kotlinx.android.synthetic.main.your_layout.view.*

class YourDialogFragment : DialogFragment() {

    private lateinit var rootView: View

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        rootView = activity.layoutInflater.inflate(R.layout.your_layout, null as ViewGroup?)

        rootView.someTextView.text = "Hello" // works
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接