30

Let's say I have this layout:

<?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>

And I use it in a 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()
    }
}

if I refer to the widget using synthetic:

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

I got this error at runtime:

java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.View android.view.View.findViewById(int)' on a null object reference at MyDialog._$_findCachedViewById(DepartmentChoiceDialog.kt:0)

I don't understand how to use synthetic in DialogFragment case. It works fine in Fragment and Activity.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Geob-o-matic
  • 5,940
  • 4
  • 35
  • 41
  • Should this be clearer in the title that it is Android, and also what the actual issue is (currently it is the topic, but not the problem in the title) – Jayson Minard Dec 28 '15 at 00:32
  • 1
    For your exception, it is always useful to have a stack trace, and to note where that trace intersects with the code you provided. – Jayson Minard Dec 28 '15 at 00:32

10 Answers10

14

I found a way that works for custom dialogs.

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
      }
    })
  }
}
zx485
  • 28,498
  • 28
  • 50
  • 59
Sabaat Ahmad
  • 522
  • 1
  • 5
  • 10
  • this solution is leaking, on rotate for example, so better add customView = null in onDestroyView – Penzzz May 28 '19 at 10:36
  • If you override both `onCreateDialog` and `onCreateView`, then the DialogFragment might crash with `AndroidRuntimeException: requestFeature() must be called before adding content`. This is happening at least for API 23 but may be true for some other APIs and circumstances. See https://stackoverflow.com/a/21734372/5035991. The safer solution is to override only `onCreateDialog`, set your view to the dialog and store it in your DialogFragment, then call synthetic features on the view: `this.dialogView.my_text_view.text = "..."`. – Mr. Goldberg Dec 28 '20 at 19:42
10

It looks like this isn't supported by default yet, but I've found the easiest way to do it to be like this. In a base dialog class:

protected abstract val containerView: View

override fun getView() = containerView

In a subclass:

override val containerView by unsafeLazy {
    View.inflate(context, R.layout.dialog_team_details, null) as ViewGroup
}

Then you can use the synthetic views as you normally would and use the containerView as the view for your dialog.

SUPERCILEX
  • 3,929
  • 4
  • 32
  • 61
  • The cleanest solution. Just a small question. Could this setup lead to memory leaks, when you save a reference to the view or does Android take care of this? – Bohsen Apr 13 '18 at 09:02
  • 3
    I use Leak Canary and haven't noticed anything. Also, just thinking about it, it wouldn't make sense for there to be a memory leak since the view is associated with an instance of a context class and will die with it in a config change. – SUPERCILEX Apr 13 '18 at 15:01
  • [Upvoted for sheer ingenuity] It certainly works. But in my case (generic Dialog within DialogFragment) I suffered a bizarre side-effect: Somehow an unspecified theme got applied to the View and I was seeing white text on a white background! – Bad Loser Sep 11 '19 at 04:31
6

Previous answer will not work, because onViewCreated is not called when you use onCreateDialog. You should first import kotlinx...department_chooser_dialog.view.dep_list, an then use it as follows:

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

So I'm not sure if this has been solved... I just came across this. If you have a custom Dialog view make a class that extends DialogFragment and use the "dialog" object to import views in the layout. I'm using Android Studio 3.1.3 and Kotlin version 1.2.41 at the time of writing.

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
  }
}
Aceofspadez44
  • 234
  • 2
  • 11
4

Change to onCreateView implementation

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

and use a custom title(TextView) and cancel(Button) in the department_chooser_dialog

onActivityCreated will run after onCreateView and will be just fine.

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    dep_list.layoutManager = LinearLayoutManager(activity)
    dep_list.itemAnimator = DefaultItemAnimator()
    dep_list.setHasFixedSize(true)
}
Derek Brown
  • 4,232
  • 4
  • 27
  • 44
2

Because the default view's value from fragment(kotlin generate method _$_findCachedViewById), but if we create View from dialog, lead to fragment view is null, so we can't directly use default xxx , but we can use dialog.xxx replace default xxx

act262
  • 463
  • 4
  • 7
1

Blockquote

The kotlin code in a Fragment like this:

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
        }
    }
}

After decompile the bytecode, you can see the implementation of synthetic properties:

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

and the _$_findCachedViewById method :

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;

}

so the magic is just the this.getView(). The Fragment.mView property is assigned after Fragment.onCreateView(inflater, container, savedInstanceState), if you use Kotlin Synthetic Properties in onCreateView() method, there will be a NPE. Code from FragmentManager.moveToState():

case Fragment.CREATED:
    ...
    f.mView = f.performCreateView(f.performGetLayoutInflater(
                                f.mSavedFragmentState), container, 
    f.mSavedFragmentState);
    ...

To fix the NPE, make sure getView method return a non-null view.

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
}

and at the onDestroyView() lifecycle callback, set mContentView to null.

override fun onDestroyView() {
    super.onDestroyView()
    mContentView = null
}
RAINA
  • 802
  • 11
  • 22
ggaier
  • 331
  • 2
  • 6
1

The setContentView is inside the OnActivityCreated calls. So by a synthetic set of controls to monitor events needs to call here:

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()
    }
}

And it worked.

jkdev
  • 11,360
  • 15
  • 54
  • 77
0

Move your code from onActivityCreated to onViewCreated method. Like this:

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)
    }
}

I actually didn't look deeper into generated code and maybe there is a bug.

-1

The views are accessible via the view that you inflate in onCreateDialog. So, if you save the view in a variable (rootView) you can access the views from any method inside of 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
    }
}
Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121