What is the recommended way to use dialogs (alias AlertDialogs)? Can I somehow treat them as controllers?
With regular fragments I am mostly wrapping Dialogs in a DialogFragment so they survive runtime changes.
Look at the dialogs from the Conductor sample app.
I was searching for dialogs before, but I found nothing.
So one would have to reconstruct the features an alert dialog gives you like detecting touches outside, overlaying and shadowing the layout below, setting material design buttons etc?
You have control over the implementation and can do it with or without a shadow. If you want things the standard way then use the out of the box system Dialog.
Like @adi1133 said, it just depends on what you're looking for. If you want something custom, the OverlayController from the demo app is a good starting point. If you want the stock Material look, use the system dialogs. Personally I use the system dialogs in production apps.
In production apps I use custom views. Use what best suits your project.
And how do you handle system dialogs with conductor? Do you wrap it in a DialogFragment? Do you just use them outside the regular Controller flow? How do you handle communication between the components?
If your create dialogs you can use the Dialog class and its descendants. They do not rely on the fragment api.
For interactions with the system you are required to use activities/intents/extras, Conductor provides some shortcut methods: like startActivityForResult() and requestPermissions().
For Android interactions that Conductor does not provide, you can access the underlying Activity or Application class using getActivity() and getApplicationContext() and call other methods or set listeners.
What is your use case?
A common use case is: The user clicks on a button, a dialog with an EditText pops up, the user enters information and clicks confirm. Then the parent fragment that implements a callback interface from the dialog fragment receives that callback and reacts.
Using a plain dialog does not seem to be a convenient solution as it handles no state restoring.
Yeah I agree this is a tricky problem. Conductor helps us build apps without fragment and I've been enjoying some nice productivity gains but dealing with fragments is still necessary, and in some cases for the better.
Some of the examples that come to mind:
The way I'm dealing with these is simply by accessing the FragmentManager from the Activity: ((AppCompatActivity) getActivity()).getSupportFragmentManager(). Things like beginTransaction and putFragment do what you'd expect. It's not pretty but it does the job and I have yet to find any major issue with this approach. I do wish there was a built in way to interop with fragments. I toyed with the idea of having a base FragmentController class that can be pushed onto the router like any other controller but I wasn't able to get everything working right at the time. Might be worth giving it another shot now that v2 is here.
Sounds like this should be provided by a third party library ... It's not
too hard, just someone has to find some hours/days to reimplement the Ui
components. Maybe the layouts can just be copy and pasted from android
framework or other third party material design libraries ...
Benoit Dion [email protected] schrieb am Mo., 12. Sep. 2016, 06:14:
Yeah I agree this is a tricky problem. Conductor helps us build apps
without fragment and I've been enjoying some nice productivity gains but
dealing with fragments is still necessary, and in some cases for the better.Some of the examples that come to mind:
- Dialogs
- Easier integration with complex controls like TimePickerFragment,
DatePickerFragment or SupportMapFragment (#109
https://github.com/bluelinelabs/Conductor/issues/109)- Integration with legacy code
The way I'm dealing with these is simply by accessing the FragmentManager
from the Activity: ((AppCompatActivity)
getActivity()).getSupportFragmentManager(). Things like beginTransaction
and putFragment do what you'd expect. It's not pretty but it does the job
and I have yet to find any major issue with this approach. I do wish there
was a built in way to interop with fragments. I toyed with the idea of
having a base FragmentController class that can be pushed onto the router
like any other controller but I wasn't able to get everything working right
at the time. Might be worth giving it another shot now that v2 is here.—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/bluelinelabs/Conductor/issues/112#issuecomment-246239596,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAjnrvWZ4qvaNt9dRYsmCvdiD-V-EOalks5qpNGYgaJpZM4J4ulV
.
@sockeqwe
Material Dialogs has about 1300 commits and has lots of lots of stylings. Just take a look on this discussion on how to implement the size correctly. I think getting this right would more likely take months and would be a very specific solution for a problem that is already solved on another level.
I also wonder if it makes sense to reimplement dialogs. DialogFragments are basically headless, but use their lifecycle and state persistence capabilities to manage dialogs. The same should be possible with Conductor controllers.
This wouldn't have to be in Conductor itself. I tried something slightly different out a while ago.
I think of dialogs as a special case for fragments. If we find a good solution for fragments we'll have something we can use for dialogs.
My solution so far should work in most of the cases.
I made a base dialog class that gets a reference to the router from its hosting activity and iterates the backstack from the end.
public abstract class ControllerDialogFragment extends DialogFragment {
@Nullable public <T> T findCallback(Class<T> toFind) {
Router router = ((RouterProvider) getActivity()).provideRouter();
List<RouterTransaction> backStack = router.getBackstack();
for (int i = backStack.size() - 1; i >= 0; i--) {
RouterTransaction routerTransaction = backStack.get(i);
Controller controller = routerTransaction.controller();
if (controller.isAttached() && toFind.isInstance(controller)) {
//noinspection unchecked
return (T) controller;
}
}
return null;
}
public interface RouterProvider {
@NonNull Router provideRouter();
}
}
@PaulWoitaschek can you show that class in use?
@sirvon I fine tuned a little bit so it finds callbacks in child routers too.
@Nullable public <T> T findCallback(Class<T> toFind) {
Router router = ((RouterProvider) getActivity()).provideRouter();
return findCallbackInRouter(toFind, router);
}
@Nullable private <T> T findCallbackInRouter(Class<T> toFind, Router router) {
List<RouterTransaction> backStack = router.getBackstack();
for (int i = backStack.size() - 1; i >= 0; i--) {
RouterTransaction routerTransaction = backStack.get(i);
Controller controller = routerTransaction.controller();
if (controller.isAttached()) {
if (toFind.isInstance(controller)) {
//noinspection unchecked
return (T) controller;
} else {
List<Router> childRouters = controller.getChildRouters();
for (Router childRouter : childRouters) {
T callback = findCallbackInRouter(toFind, childRouter);
if (callback != null) return callback;
}
}
}
}
return null;
}
To use it you just create your Dialog
class MyDialog extends ControllerDialogFragment {
void fireCallback(){
Callback callback = findCallback(Callback.class);
if(callback!=null) callback.onReady();
}
interface Callback {
void onReady();
}
}
And let your controller implement that callback.
class MyController extends Controller implements MyDialog.Callback {
@Override
void onReady(){
// dialog callback fired
}
}
In the meantime this solution fails in some cases. One case I had is that I have multiple controllers visible at the same time who all needed to get the callback from the dialog.
The solution is to tell the controller that needs to get the callback from the dialog which tag its transaction has and to pass this tag to the dialog which will then check for the transaction tag when finding the callback.
But yes, having a way to interact with framework components would be really good as this is a kind-of too complicated solution.
This solution does not really work with view pagers.
Okay 3rd post in a row ;-)
A possible workaround for me was to use an EventBus. But that complicated my code unnecessarily as my components were no longer able to be agnostic of where they were used.
So the pattern that evolved for me is to retrieve the controller through the instance id.
So I have my base dialog which holds has a key that refers to its arguments
abstract class BaseDialogFragment extends DialogFragment {
protected static final String NI_CONTROLLER_ID = "ni#controllerId";
@NonNull protected <T> T findCallback() {
Router router = ((RouterProvider) getActivity()).provideRouter();
String controllerId = getArguments().getString(NI_CONTROLLER_ID);
//noinspection unchecked
return (T) router.getControllerWithInstanceId(controllerId);
}
}
So whenever someone calls findCallback() it assumes the arguments contain the instance id and casts unchecked to the desired callback.
Child classes that have a callback then simply take the controller which also implements the interface as an argument in its static factory
class CallbackDialog extends BaseDialogFragment {
@NonNull @CheckResult public static <T extends Controller & Callback> CallbackDialog newInstance(T controller) {
Bundle args = new Bundle();
args.putString(NI_CONTROLLER_ID, controller.getInstanceId());
CallbackDialog fragment = new CallbackDialog();
fragment.setArguments(args);
return fragment;
}
void fireCallback() {
Callback callback = findCallback();
callback.fire();
}
interface Callback {
void fire();
}
}
I tried to wrap the dialog in Controller, like DialogFragment does it. Please, look my pull request #248
I find the idea very interesting. However there are some flaws.
The state restoring should happen in onSaveViewState / onRestoreViewState. The instance restoring is happening before the dialog is being created so this will crash immediately upon orientation change.
Also the background-dim of the dialog is causing the whole view to flash upon each orientation change.
@PaulWoitaschek thank you for your comment. I fixed the restoring/saving of the dialog state. What about second flaw, the blinking not occurring with Conductor 2.1.1
My bad, it is because I removed the fade change handler.
Another thing: Instead of subclassing the Controller I would subclass RestoreViewOnCreateController and pass the Bundle savedState to onCreateDialog instead of the activity. That would be consistent with how DialogFragments work. Not sure about this though, what are your opinions?
At least I find the context pretty random, people can just call getActivity.
Also I suggest to make onCreateView final as it's really not designed for overriding like this.
I thing that the subclassing RestoreViewOnCreateController is good idea. I did not know that it exists.
I consider the parameter Сontext is needed for the following reasons:
1) Always when creating a dialog via the builder, you need a Context
2) To avoid the warning from Lint: "Argument getActivity() might be null"
3) It's just convenient
It does not exist in other places of the library. I for example always use a context that is wrapped by a ContextThemeWrapper and never the context of the activity.
The Controller is missing getContext method. It would be
a good place for wrapping the Context.
Current dialog solution in demo app it's just one view over another. So, this implementation has extra overdraw. I think need implementation based on DIalog like DialogFragment does.
what's the general opinion on the best solution for this?
For me it works well to wrap the dialog inside a controller. There is an open PR for that.
I made a BaseController than may be shown as usual or as a dialog similarly to DialogFragment. The idea is to return an empty View from onCreateView and use a custom ControllerChangeHandler to make sure previous controller view is not removed.
Implementation is visible here https://github.com/SamYStudiO/beaver/blob/master/app/src/main/kotlin/net/samystudio/beaver/ui/base/controller/BaseController.kt
I'll be happy to have feedback about it!
Why is this necessary? Cant you just show dialog via AlertDialog.show?
Btw the recommended library for dia
logs in README does show dialog but conductor replaces views so view under dialog is gone, which kind of defeats the purpose of dialogs?
Why is this necessary? Cant you just show dialog via AlertDialog.show?
It doesn't handle state restoration. When you rotate the phone, the dialog is gone and all it's inputs.
but conductor replaces views so view under dialog is gone, which kind of defeats the purpose of dialogs?
How are you using it? The view should not get replaced.
@ursusursus Hi
Use this change handler
transaction.pushChangeHandler(new SimpleSwapChangeHandler(false));
transaction.popChangeHandler(new SimpleSwapChangeHandler());
Use .show()
@MkhytarMkhoian oh I missed the "false" argument, Im starting out with conductor, I had it that change handler is just a animation decoration kind of thing, not actually addView, removeView as now I see. Thanks!
For anyone interested in dialogs with v3.0.0-rc4+, I've forked the conductor-dialog lib [here] (https://github.com/patjackson52/conductor-dialog/tree/master).
I have an open PR into the original repo as well.
The interaction between a DialogFragment and a Controller is simple if the app is already following MVVM. The model is managed by the view model and the view (both Dialog and the Controller are the View im MVVM) simply updates the view model / model so no direct communication between the Dialog and the Controller is needed.
Here's an example of a DialogFragment in one of my apps. I'm using Toothpick for dependency injection but any other framework will work too.
class NewMatchDialog : DialogFragment() {
private val viewModel: MatchViewModel by inject()
override fun onCreateDialog(savedInstanceState: Bundle?) = context?.run {
KTP.openRootScope().openSubScope(APP_SCOPE).inject(this@NewMatchDialog)
val layout = LayoutInflater.from(this).inflate(R.layout.match_names, null)
AlertDialog.Builder(this)
.setTitle(R.string.new_match_title)
.setView(layout)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name1 = layout.findViewById<EditText>(R.id.team_name_1).text.toString()
val name2 = layout.findViewById<EditText>(R.id.team_name_2).text.toString()
viewModel.createMatch(name1, name2)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dismiss())
}
.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss()}
.create()
} ?: Dialog(context!!) // yeah yeah not properly done here
}
Most helpful comment
@sirvon I fine tuned a little bit so it finds callbacks in child routers too.
To use it you just create your Dialog
And let your controller implement that callback.