What is the proper way to handle callback effects and errors in Compose? - android-jetpack

I have a main composable that wraps a NavHost to display different screens. Some of these screens need to navigate to other screens based on state changes in a ViewModel that happen as a result of method calls. Here's a trimmed down example of what I have at the moment:
class ExampleViewModel(application: Application) : AndroidViewModel(application) {
// Used by a
var error: String? by mutableStateOf(null)
private set
var user: User? by mutableStateOf(null)
private set
fun onLogin(email: String, password: String) {
viewModelScope.launch {
doLogin(email, password)
.onSuccess { user = it }
.onFailure { error = it.localizedMessage }
}
}
}
#Composable
fun LoginScreen(
navController: NavController,
exampleViewModel: ExampleViewModel,
) {
DisposableEffect(exampleViewModel.user) {
if (exampleViewModel.user != null) {
navController.navigate("somewhere")
}
onDispose {}
}
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
// Email TextField.
// Password TextField.
Button(onClick = { exampleViewModel.onLogin(email, password) }) {
Text("Login")
}
}
The error is handled like this in a composable up above:
LaunchedEffect(exampleViewModel.error) {
exampleViewModel.error?.let { scaffoldState.snackbarHostState.showSnackbar(it) }
}
Using a DisposableEffect in this way seems kind of dirty, and quite error prone. On top of that, this error handling method makes it difficult to, for example, disable the login form while the login is pending. Would it be better to make onLogin() suspend and handle its success and failures, and corresponding local state, in a callback inside of LoginScreen? The downside to that is that the login screen will no longer automatically redirect if it's navigated to while already logged in. snapshotFlow in a LaunchedEffect(true) is another thing I've considered, but that doesn't really seem to have any particular benefits over DisposableEffect.
What's the correct way to do this? Am I on completely the wrong track here?

Related

Await method before app starts in the same UI thread

I'm trying to check which page should load my app at the beginning, first of all I check a database table if I find the login information stored I want to push the once named StartPage(), as I'm working with the database the method includes an await if there isn't any data stored I want to push the LoginPage(). I have tried following this example Xamarin.Forms Async Task On Startup . My code is :
public App()
{
int result;
InitializeComponent();
ThreadHelper.Init(SynchronizationContext.Current);
ThreadHelper.RunOnUIThread(async () => {
MainPage = new ActivityIndicatorPage();
result = await InitializeAppAsync();
if (result == 0)
{
PushLoginPage();
}
else
{
PushStartPage();
}
});
}
public void PushStartPage()
{
NavigationPage nav = new NavigationPage(new StartPage());
nav.SetValue(NavigationPage.BarBackgroundColorProperty, Color.FromHex("#D60000"));
MainPage = nav;
}
public void PushLoginPage()
{
MainPage = new Login();
}
public void PushLoginPage(string email, string password)
{
MainPage = new Login(email, password);
}
private async Task<int> InitializeAppAsync()
{
if (ViewModel == null)
ViewModel = new MainViewModel(this);
return await ViewModel.LoginViewModel.PushInitialPage();
}
But throws the following exception and as the author of the article says, is not recommended to do it.
Exception
Another option tried was overriding the OnStart() method but didn't work either.
protected override async void OnStart()
{
Task.Run(async ()=> { await InitializeAppAsync(); });
}
The PushInitialPage method:
public async Task PushInitialPage()
{
if (_app.Properties.ContainsKey("isLogged"))
{
var user = await UserDataBase.GetUserDataAsync();
var result = await Login(user.Email, user.Password);
if (result.StatusCode != 200)
{
return 0;
///PushLoginPage();
}
else
{
return 1;
//PushStartPage();
}
}
else
{
return 0;
}
}
When the OS asks your app to show a page, it must show a page. It can't say "hold on a minute or two while I talk to this remote server over an iffy network connection." It has to show a page Right Now.
So, I recommend bringing up a splash page - your company or app logo, for example. When the splash page shows, then call InitializeAppAsync, and based on the result, switch to the login or start page or nice user-friendly offline error page.
In Xamarin.Forms we have properties called 'Application.Current.Properties'. By using this we can able to save the any data type. So once user login in to the application you can set one flag and set it is true. Then after every time when user login in to the application you can check this flag and navigate your respective page.
Sample Code :
App.cs :
public App()
{
if (Current.Properties.ContainsKey("isLogged"))
{
if((bool)Application.Current.Properties["isLogged"])
{
// navigate to your required page.
}
else
{
// naviate to login page.
}
}
else
{
// naviate to login page.
}
}
At first time application open it checks the 'isLogged' property is presented or not, if not it will move to the login page. When user login into the application by using his credentials, we need to create 'isLoggin' property and set as true. Then after if user try to login it checks the condition and navigates to the respective page.
Saving Property SampleCode :
Application.Current.Properties["isLogged"] = true;
await Application.Current.SavePropertiesAsync();
write above code for after login into the application. If a user log out from the app you need to set 'isLogged' flag is false.

SignalR - correct implementation of chat

I need to implement chat on my web project. How to implement it on one page - there are many articles about it. But I need to have ability :
1. Notify other users, that somebody logged to site (on any page, not only on chat page)
2. Notify other users, that somebody logout
So, I have the following code of hub:
public void Connect()
{
try
{
var id = Context.ConnectionId;
string username = Context.User.Identity.Name;
var currentUser = connectedUsers.Where(p => p.Username == username).FirstOrDefault();
if (currentUser == null)
{
AddNewUserToCollection();
}
else
{
// update ConnectionId for sure (connection id is changed sometimes (probably if user is logged out and login again))
if (currentUser.ConnectionId != id)
{
var companyId = _chatRepository.GetCompanyIdOfUser(username); // throws exception if companyId is null
Groups.Remove(currentUser.ConnectionId, companyId.ToString());
Groups.Add(id, companyId.ToString());
currentUser.ConnectionId = id;
//Clients.Group(companyId.ToString()).onNewUserConnected(username);
}
}
}
catch(InvalidCompanyException c_ex)
{
Clients.Client(Context.ConnectionId).onErrorMessage($"User '{c_ex.Username}' does not exist");
}
}
public void Disconnect()
{
string username = Context.User.Identity.Name;
var item = connectedUsers.Where(p => p.Username == username).FirstOrDefault();
if (item != null)
{
connectedUsers.Remove(item);
Groups.Remove(item.ConnectionId, item.CompanyID.ToString());
Clients.Group(item.CompanyID.ToString()).onUserDisconnected(item.Username);
}
}
public override Task OnDisconnected(bool stopCalled)
{
var item = connectedUsers.Where(p => p.ConnectionId == Context.ConnectionId).FirstOrDefault();
if (item != null)
{
connectedUsers.Remove(item);
Groups.Remove(item.ConnectionId, item.CompanyID.ToString());
Clients.Group(item.CompanyID.ToString()).onUserDisconnected(item.Username);
}
return base.OnDisconnected(stopCalled);
}
and I added the following code to _layout.cshtml:
<script>
$(document).ready(function () {
var chat = $.connection.chatHub;
$.connection.hub.start().done(function () {
chat.server.connect();
});
});
</script>
to notify other users, that the current user is logged. But debugger says, that pair OnDisconnected/Connect is called every time, when user reload page (go thru pages) with different connectionId. When I remove this client code - that pair is not called. How to implement it correctly, to notify other users, that somebody is online, but without reconnect each time?
Since you have the connect() call in a razor (_layout.cshtml) page, you will effectively be disconnecting/connecting on every post back (page load).
The scenario you're trying to use is best in a SPA (or AJAX) scenario where navigation is handled asynchronously by client side JavaScript. Your current setup is refreshing the screen, reloading the JavaScript, re-running the document ready() function with each server rendered navigation.
Another alternative is to use the client's actual user Id, and pass that to the server.connect(id) method. Then use this user Id to track the user activity instead of the hub ConnectionId.

Checking if page is valid without firing validations from server side

I want to check if current page is valid for a validation group without showing any validation messages. If I call Page.Validate("ValidationGroup") then it shows all the valiation message but I do not want it.
I just to check if page is valid for a particular validation group. And one more constraint, I have to do this on server side can't do it with javascript.
You can loop through the validators in a validation group, like this:
private bool AreAllValidatorsInGroupValid(string validationGroupName)
{
foreach(var theValidator in Page.GetValidators(validationGroupName))
{
var baseValidator = theValidator as BaseValidator;
if(baseValidator != null)
{
if(!baseValidator.IsValid)
{
return false;
}
}
}
return true;
}
Usage:
if(!AreAllValidatorsInGroupValid("ValidationGroup"))
{
// Do something here
}

About Response.Redirect, FormsAuthentication and MVC

I want to understand the difference in the behavior of a program when we call FormsAuthentication.RedirectFromLoginPage vs. when we call Response.Redirect(FormsAuthentication.GetRedirectUrl()) and manually redirect.
Please see the comments below.
I have a LoginController/Index (two actions, one for HttpGet and one for HttpPost). The View of this controller represents the application's login page.
I also have a home page or landing page, i.e. the page that the user must be taken to after a successful login. This is represented in my application by the HomeController's Index action and the ~Views/Home/Index.cshtml view.
I have presented three scenarios. I understand scenario 1 and I expect it to work the way it does, but I noted a difference in scenarios 2 and 3.
Scenario 1
namespace Controllers
{
[AllowAnonymous]
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index(Login loginViewModel)
{
if (ModelState.IsValid)
{
var user = ValidateUser(loginViewModel);
if (user != null)
{
// Other stuff: set cookies, session state, etc.
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Invalid password. Please try again.");
}
}
// If the user was a valid user, the flow-of-control won't reach here
// as expected and the user will be taken to the view that is served
// by the HomeController::Index() action. If it is by convention, it will
// be the ~Views/Home/Index.cshtml view. This is fine.
return View();
}
}
}
Scenario 2
namespace Controllers
{
[AllowAnonymous]
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index(Login loginViewModel)
{
if (ModelState.IsValid)
{
var user = ValidateUser(loginViewModel);
if (user != null)
{
// Other stuff: set cookies, session state, etc.
Response.Redirect(FormsAuthentication.GetRedirectUrl(loginViewModel.UserName,
loginViewModel.RememberMe));
}
else
{
ModelState.AddModelError("", "Invalid password. Please try again.");
}
}
// If the user was a valid user, the flow-of-control still reaches here
// as expected. And as expected, it renders the same View, i.e. the View
// associated with the controller we are in, which is ~Views/Login/Index,
// which represents the login page. This is wrong. I shouldn't redirect here.
// I understand this. My question here is two fold:
// 1) I am simply trying to understand the difference in behaviors of the three
// scenarios described in this question.
// 2) Given this, the right way would be to not use Response.Redirect here but instead
// use RedirectToAction. However, if I wanted to use Response.Redirect, what should
// I do?
return View();
}
}
}
Scenario 3
namespace Controllers
{
[AllowAnonymous]
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index(Login loginViewModel)
{
if (ModelState.IsValid)
{
var user = ValidateUser(loginViewModel);
if (user != null)
{
// Other stuff: set cookies, session state, etc.
FormsAuthentication.RedirectFromLoginPage(loginViewModel.UserName,
loginViewModel.RememberMe);
}
else
{
ModelState.AddModelError("", "Invalid password. Please try again.");
}
}
// If the user was a valid user, the flow-of-control still reaches here
// as expected. However, magically, somehow, even though the statement below
// suggests that the user must be taken to the View of the same controller and
// action that we are currently in, i.e. the View of the LoginController::Index()
// action, i.e. the ~Views/Login/Index.cshtml, it magically takes me to the
// ~Views/Home/Index.cshtml instead, which is what is specified as the LoginPage
// attribute of the <authentication>/<forms> element in the web.config.
// I want to know how this happens.
return View();
}
}
}
Update
I am at my wit's end now. Now, even Scenario 1 that uses RedirectToAction is calling the Index() action on the LoginController class.
The actual difference is that FormsAuthentication.RedirectFromLoginPage() sets cookies and then makes redirects but FormsAuthentication.GetRedirectUrl() only returns redirect url.
The funny thing is that implementation of FormsAuthentication.GetRedirectUrl() is like this:
public static String GetRedirectUrl(String userName, bool createPersistentCookie)
{
if (userName == null)
return null;
return GetReturnUrl(true);
}
So actually userName and createPersistentCookie parameters are completely ignored. You must call FormsAuthentication.SetAuthCookie( userName, true/false ) manually before calling GetRedirectUrl.
Agree with Vasily.
RedirectFromLoginPage issues an authentication ticket and places it in the default cookie using the SetAuthCookie method.
You can read something about this behavior here.
If you want to have a better control over the cookie creation you should (encryption, expiration, extending the principal) you should create the cookie yourself.
I explained the whole process here and here.

Check if user exists in Active Directory

I need to check if an user exists in AD and if so, retrieve some user information. I have been able to do this as shown below. But, it is very slow. Is there any way to do this faster?
Thanks!
using System;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
namespace ConsoleApplication1
{
class Program
{
static void Main()
{
Console.WriteLine("Enter AD account name...");
string strADLoginName = Console.ReadLine();
using(PrincipalContext context = new PrincipalContext(ContextType.Domain,"DEVMC"))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(context, strADLoginName))
{
bool userExists = (user != null);
if (userExists)
{
Console.WriteLine("User exists");
Console.WriteLine(user.EmailAddress);
}
else
{
Console.WriteLine("User doesn't exist");
}
}
}
Console.ReadKey();
}
}
}
Well, the only real approach you could tak to make this faster would be to have the "PrincipalContext" be constructed once somewhere and cached for future use, so you don't have to re-create that context over and over again, every time you call that function.
Other than that - no, I don't see much room for improvement right here and now. What kind of app is this?? ASP.NET web apps, or Winforms, WPF, Silverlight??

Resources