I am rewriting an app that supports GalaSoft.MVVMLight in Xamarin.Android to Prism.
This Android project have this particular class GalaSoft.MVVMLight AppCompatActivityBase which inherits from AppCompatActivity.
public class AppCompatActivityBase : AppCompatActivity
{
public AppCompatActivityBase()
{
}
internal string ActivityKey
{
get;
private set;
}
/// <summary>
/// The activity that is currently in the foreground.
/// </summary>
public static AppCompatActivityBase CurrentActivity
{
get;
private set;
}
internal static string NextPageKey
{
get;
set;
}
/// <summary>
/// If possible, discards the current page and displays the previous page
/// on the navigation stack.
/// </summary>
public static void GoBack()
{
if (CurrentActivity != null)
{
CurrentActivity.OnBackPressed();
}
}
/// <summary>
/// Overrides <see cref="M:Android.App.Activity.OnResume" />. If you override
/// this method in your own Activities, make sure to call
/// base.OnResume to allow the <see cref="T:GalaSoft.MvvmLight.Views.NavigationService" />
/// to work properly.
/// </summary>
protected override void OnResume()
{
CurrentActivity = this;
if (string.IsNullOrEmpty(ActivityKey))
{
ActivityKey = NextPageKey;
NextPageKey = null;
}
base.OnResume();
}
/// <summary>
/// Overrides <see cref="M:Android.App.Activity.OnCreate" />. If you override
/// this method in your own Activities, make sure to call
/// base.OnCreate to allow the <see cref="T:GalaSoft.MvvmLight.Views.NavigationService" />
/// to work properly.
/// </summary>
protected override void OnCreate(Bundle savedInstanceState)
{
// Set CurrentActivity to the first activity that is created
if (CurrentActivity == null)
{
CurrentActivity = this;
if (string.IsNullOrEmpty(ActivityKey))
{
ActivityKey = NextPageKey;
NextPageKey = null;
}
}
base.OnCreate(savedInstanceState);
}
}
This AppCompatActivityBase is then referred to in a user preference class as shown below.
class UserPreferences : IUserPreferences
{
const int MaximumHistoryEntries = 5;
readonly ISharedPreferences preferences;
readonly ISharedPreferencesEditor editor;
public UserPreferences()
{
preferences = PreferenceManager.GetDefaultSharedPreferences(AppCompatActivityBase.CurrentActivity.ApplicationContext);
editor = preferences.Edit();
}
public event EventHandler Saved;
public string AssetsExtractedVersion
{
get
{
return preferences.GetString(AppCompatActivityBase.CurrentActivity.GetString(Resource.String.PrefKeyAssetsExtractedVersion), null);
}
set
{
editor.PutString(AppCompatActivityBase.CurrentActivity.GetString(Resource.String.PrefKeyAssetsExtractedVersion), value);
}
}
public void Save()
{
editor.Commit();
Saved?.Invoke(this, EventArgs.Empty);
}
}
Since my project now supports Prism instead of GalaSoft.MVVMLight. How can I replace AppCompatActivityBase in Prism? My background is in iOS so I'm having trouble understanding the Android end of Xamarin/Prism.
There isn't one. While Prism does have an Android specific binary to support Dependency Injection with the Xamarin.Forms Dependency Resolver which may include a Android.Content.Context, Prism itself is entirely dependent on Xamarin.Forms and runs the exact same across all platforms. As a result there is no need for a Prism specific base Activity
Related
How do I convert an Android lifecycle callback to a RX Observable?
For example, onNewIntent. I could make my Activity itself a custom Observable, calling onNext for a bunch of Observers within onNewIntent - but that feels yucky. I would rather not implement a home-brew Observable if I can help it, so as to not need to mess with multi-threading etc.
namespace ...
{
using ...
/// <summary>
/// Main application activity.
/// </summary>
[Activity(...)]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity, IObservable<Intent>
{
private List<IObserver<Intent>> observers;
...
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<Intent> observer)
{
this.observers.Add(observer);
return new Unsubscriber(this.observers, observer);
}
/// <inheritdoc/>
protected override void OnNewIntent(Intent intent)
{
foreach (var observer in this.observers)
{
observer.OnNext(intent);
}
}
private class Unsubscriber : IDisposable
{
private List<IObserver<Intent>> observers;
private IObserver<Intent> observer;
private bool disposedValue;
public Unsubscriber(
List<IObserver<Intent>> observers,
IObserver<Intent> observer)
{
this.observers = observers;
this.observer = observer;
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
// dispose managed state (managed objects)
if (disposing &&
this.observer != null &&
this.observers.Contains(this.observer))
{
this.observers.Remove(this.observer);
}
// set large fields to null
this.observers = null;
this.observer = null;
this.disposedValue = true;
}
}
}
}
}
If there was something like a NewIntent event, this would be an easier question - there are creation operators for events, delegates, etc. But these all work with the event from outside the event - with something like an onNewIntent override implementation, I'm already inside the event (inside the monad?). So I don't know how to invert that.
I've come up with a solution. Not sure it's the most elegant, but it basically involves creating an additional event that I raise when handling the callback. Then it's just a quick trip to FromEventPattern and hey presto! Observable event data. Would be happy to hear if anyone else has any alternate approaches, but this seems to work.
namespace ...
{
using ...
/// <summary>
/// Main application activity.
/// </summary>
[Activity(...)]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity, IWithNewIntentObservable
{
private event EventHandler<Intent> NewIntent;
/// <inheritdoc/>
public IObservable<Intent> NewIntentObservable { get; private set; }
...
/// <inheritdoc/>
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
...
this.NewIntentObservable = Observable.FromEventPattern<Intent>(
h => this.NewIntent += h,
h => this.NewIntent -= h).Select(e => e.EventArgs);
}
/// <inheritdoc/>
protected override void OnNewIntent(Intent intent)
{
this.NewIntent?.Invoke(this, intent);
}
}
}
We have an MvxTabbedPage and child MvxContentPages in our Xamarin.Forms project.
On Android, I'm finding that the ViewAppeared override on my first child page is not being called the first time the MvxTabbedPage is being shown.
When switching tabs, it subsequently is being called correctly.
I'm initialising the PageModels in the ViewAppearing for the MvxTabbedPage's PageModel as below:
public override async void ViewAppearing()
{
await ShowInitialViewModels();
base.ViewAppearing();
}
private bool viewModelsInitialised = false;
private async Task ShowInitialViewModels()
{
if (!viewModelsInitialised)
{
await _BusyManager.SetBusy();
var tasks = new List<Task>();
tasks.Add(_MvxNavigationService.Navigate<HomePageModel>());
tasks.Add(_MvxNavigationService.Navigate<MyBenefitsPageModel>());
tasks.Add(_MvxNavigationService.Navigate<ClaimsPageModel>());
tasks.Add(_MvxNavigationService.Navigate<ContactUsPageModel>());
tasks.Add(_MvxNavigationService.Navigate<SettingsPageModel>());
await Task.WhenAll(tasks);
viewModelsInitialised = true;
await _BusyManager.SetUnBusy();
}
}
Have others seen this behaviour, and/or should I be doing something differently?
Looks like it's this Forms bug:
https://github.com/xamarin/Xamarin.Forms/issues/3855
which is referenced by this MvvmCross issue
https://github.com/MvvmCross/MvvmCross/issues/2823
(thanks to Pedro for pointing me in this direction on Slack:)
Check the Playground project of mvvmcross. You should manage your tabs initializations separately in a viewmodel and the XF view code behind.
public class YourTabsViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
public YourTabsViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService;
ShowInitialViewModelsCommand = new MvxAsyncCommand(ShowInitialViewModels);
}
public IMvxAsyncCommand ShowInitialViewModelsCommand { get; private set; }
private async Task ShowInitialViewModels()
{
var tasks = new List<Task>
{
tasks.Add(_navigationService.Navigate<HomePageModel>();
tasks.Add(_navigationService.Navigate<MyBenefitsPageModel>());
tasks.Add(_navigationService.Navigate<ClaimsPageModel>());
tasks.Add(_navigationService.Navigate<ContactUsPageModel>());
tasks.Add(_navigationService.Navigate<SettingsPageModel>());
}
await Task.WhenAll(tasks);
}
}
And then on the code behind of your XF view
[MvxTabbedPagePresentation(TabbedPosition.Root, NoHistory = true)]
public partial class YourTabsPage : MvxTabbedPage<YourTabsViewModel>
{
public YourTabsPage()
{
InitializeComponent();
}
private bool _firstTime = true;
protected override void OnAppearing()
{
base.OnAppearing();
if (_firstTime)
{
ViewModel.ShowInitialViewModelsCommand.ExecuteAsync(null);
_firstTime = false;
}
}
}
I'm new to Xamarin and tried some beginner tutorials so far.
Now I wanted to build some custom stuff and need to request the current location.
For this I installed Xamarin.GooglePlayServices.Location in order to have access to the FusedLocationProviderClient class.
In MainActivity I retrieve the FusedLocationProviderClient:
public class MainActivity : FormsAppCompatActivity, ILocationProvider
{
private FusedLocationProviderClient FusedLocationClient { get; set; }
protected override void OnCreate(Bundle savedInstanceState)
{
// snip
FusedLocationClient = LocationServices.GetFusedLocationProviderClient(this);
}
}
The ILocationProvider needs the method TaskCompletionSource<Model.Location> GetPosition() to be implemented:
public TaskCompletionSource<Model.Location> GetPosition()
{
// snip
if (CheckSelfPermission(Manifest.Permission_group.Location) == Permission.Granted)
{
FusedLocationClient.LastLocation.AddOnCompleteListener(new OnLocationRequestCompleteListener(this, tcs));
}
// snip
}
The UI has a button to request the current location.
Whenever the user clicks the button I get the ILocationProvider via DependencyService and execute the GetPosition method:
private void GetPositionClicked(object sender, EventArgs e)
{
var provider = DependencyService.Get<ILocationProvider>();
if(provider != null)
{
var tcs = provider.GetPosition();
// snip
}
}
The problem now is that the application crashes as soon as I try to execute the CheckSelfPermission method in GetPosition().
I set a breakpoint in GetPosition() and noticed that FusedLocationClient is null although it clearly wasn't null after OnCreate was called.
I then inspected this in OnCreate and GetPosition and noticed that they weren't the same instances which leads me to the conclusion that something is clearly wrong here.
To solve this for now I did the following:
public class MainActivity : FormsAppCompatActivity, ILocationProvider
{
internal static MainActivity Instance { get; private set; }
private FusedLocationProviderClient FusedLocationClient { get; set; }
protected override void OnCreate(Bundle savedInstanceState)
{
// snip
Instance = this
// snip
}
// snip
public TaskCompletionSource<Model.Location> GetPosition()
{
if (this != Instance)
{
return Instance.GetPosition();
}
// snip
}
}
At least this works for now but I can't imagine that this is the way to go.
What I learned so far is that DependencyService seems to not get already created instances but create one instead (at least once).
What would be the correct way to call methods in MainActivity from the shared .NET Standard library?
Move those methods out of your Activity class into a standalone class (within your Xamarin.Android application (or a Xamarin.Android library project).
Assuming an interface like:
public interface ILocationProvider
{
Task<Tuple<double, double>> GetPosition();
}
Android Implementation:
public class Location_Android: Java.Lang.Object, ILocationProvider
{
private FusedLocationProviderClient FusedLocationClient { get; set; }
public Location_Android()
{
FusedLocationClient = new FusedLocationProviderClient(Application.Context);
}
public async Task<Tuple<double, double>> GetPosition()
{
var loc = await FusedLocationClient?.GetLastLocationAsync();
return new Tuple<double, double>(loc.Latitude, loc.Longitude);
}
}
In my android app,im following architecture components with mvvm pattern.
my app makes a network call to display the weather information.api call is being made from repository which returns a livedata of response to the viewmodel,which inturn is observed by my main activity.
the app works fine except for one condition,whenever i disconnect the internet to test the fail case,it inflates error view as required
in the error view i have a retry button,which makes the method call to observe the viewmodel again(this method was also called by oncreate() for the first time,which worked)
even after switching on the internet,and clicking the retry button which listens for the observable.still the data becomes null.
i dont know why.please anyone help
REPOSITORY
#Singleton public class ContentRepository {
#Inject AppUtils mAppUtils;
private RESTService mApiService;
#Inject public ContentRepository(RESTService mApiService) {
this.mApiService = mApiService;
}
public MutableLiveData<ApiResponse<WeatherModel>> getWeatherListData() {
final MutableLiveData<ApiResponse<WeatherModel>> weatherListData = new MutableLiveData<>();
mApiService.getWeatherList().enqueue(new Callback<WeatherModel>() {
#Override public void onResponse(Call<WeatherModel> call, Response<WeatherModel> response) {
weatherListData.setValue(new ApiResponse<>(response.body()));
}
#Override public void onFailure(Call<WeatherModel> call, Throwable t) {
weatherListData.setValue(new ApiResponse<>(t));
}
});
return weatherListData;
}
}
VIEWMODEL
public class HomeViewModel extends AndroidViewModel {
private final LiveData<ApiResponse<WeatherModel>> weatherListObservable;
#Inject public HomeViewModel(Application application, ContentRepository contentRepository) {
super(application);
this.weatherListObservable = contentRepository.getWeatherListData();
}
public LiveData<ApiResponse<WeatherModel>> getWeatherListObservable() {
return weatherListObservable;
}
}
OBSERVE METHOD IN ACTIVITY
private void observeViewModel() {
mHomeViewModel = ViewModelProviders.of(this, mViewModelFactory).get(HomeViewModel.class);
mHomeViewModel.getWeatherListObservable().observe(this, weatherModelApiResponse -> {
if (weatherModelApiResponse.isSuccessful()) {
mErrorView.setVisibility(View.GONE);
mBinding.ivLoading.setVisibility(View.GONE);
try {
setDataToViews(weatherModelApiResponse.getData());
} catch (ParseException e) {
e.printStackTrace();
}
} else if (!weatherModelApiResponse.isSuccessful()) {
mBinding.ivLoading.setVisibility(View.GONE);
mDialogUtils.showToast(this, weatherModelApiResponse.getError().getMessage());
mErrorView.setVisibility(View.VISIBLE);
}
});
}
RETRY BUTTON IN ACTIVITY
#Override public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_retry:
mErrorView.setVisibility(View.GONE);
observeViewModel();
break;
}
}
Updated:- 5 December 2017
I was fortunate to meet Lyla Fujiwara, during Google Developer Days, India where I asked her the same question. She suggested me to user Transformations.switchMap(). Following is the updated solution -
#Singleton
public class SplashScreenViewModel extends AndroidViewModel {
private final APIClient apiClient;
// This is the observable which listens for the changes
// Using 'Void' since the get method doesn't need any parameters. If you need to pass any String, or class
// you can add that here
private MutableLiveData<Void> networkInfoObservable;
// This LiveData contains the information required to populate the UI
private LiveData<Resource<NetworkInformation>> networkInformationLiveData;
#Inject
SplashScreenViewModel(#NonNull APIClient apiClient, #NonNull Application application) {
super(application);
this.apiClient = apiClient;
// Initializing the observable with empty data
networkInfoObservable = new MutableLiveData<Void>();
// Using the Transformation switchMap to listen when the data changes happen, whenever data
// changes happen, we update the LiveData object which we are observing in the MainActivity.
networkInformationLiveData = Transformations.switchMap(networkInfoObservable, input -> apiClient.getNetworkInformation());
}
/**
* Function to get LiveData Observable for NetworkInformation class
* #return LiveData<Resource<NetworkInformation>>
*/
public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
return networkInformationLiveData;
}
/**
* Whenever we want to reload the networkInformationLiveData, we update the mutable LiveData's value
* which in turn calls the `Transformations.switchMap()` function and updates the data and we get
* call back
*/
public void setNetworkInformation() {
networkInfoObservable.setValue(null);
}
}
The Activity's code will be updated as -
final SplashScreenViewModel splashScreenViewModel =
ViewModelProviders.of(this, viewModelFactory).get(SplashScreenViewModel.class);
observeViewModel(splashScreenViewModel);
// This function will ensure that Transformation.switchMap() function is called
splashScreenViewModel.setNetworkInformation();
This looks the most prominent and proper solution to me for now, I will update the answer if I better solution later.
Watch her droidCon NYC video for more information on LiveData. The official Google repository for LiveData is https://github.com/googlesamples/android-architecture-components/ look for GithubBrowserSample project.
Old Code
I have not been able find a proper solution to this, but this works so far -
Declare ViewModel outside the observeViewModel() and change the function like this -
private void observeViewModel(final HomeViewModel homeViewModel) {
homeViewModel.getWeatherListObservable().observe(this, weatherModelApiResponse -> {
if (weatherModelApiResponse.isSuccessful()) {
mErrorView.setVisibility(View.GONE);
mBinding.ivLoading.setVisibility(View.GONE);
try {
setDataToViews(weatherModelApiResponse.getData());
} catch (ParseException e) {
e.printStackTrace();
}
} else if (!weatherModelApiResponse.isSuccessful()) {
mBinding.ivLoading.setVisibility(View.GONE);
mDialogUtils.showToast(this, weatherModelApiResponse.getError().getMessage());
mErrorView.setVisibility(View.VISIBLE);
}
});
}
Update HomeViewModel to -
public class HomeViewModel extends AndroidViewModel {
private final LiveData<ApiResponse<WeatherModel>> weatherListObservable;
#Inject public HomeViewModel(Application application, ContentRepository contentRepository) {
super(application);
getWeattherListData();
}
public void getWeatherListData() {
this.weatherListObservable = contentRepository.getWeatherListData();
}
public LiveData<ApiResponse<WeatherModel>> getWeatherListObservable() {
return weatherListObservable;
}
}
Now Retry button, call the observeViewModel function again and pass mHomeViewModel to it. Now you should be able to get a response.
I have a problem with the dependency services for implementing features that depends of the plattorm. I need what my implementation on Android receive a Context object to do the task. How can I do it?
This is my code:
1) On PCL:
public interface ICallService
{
List<string> GetContacts();
}
2) On Android Project:
[assembly: Dependency(typeof(CallService))]
namespace DEMOBLOBS.Droid.DependencyServicesPruebas
{
public class CallService : ICallService
{
public static void Init() { }
public List<string> GetContacts()
{
AT THIS POINT I NEED THE CONTEXT OBJECT!
}
}
}
The constructor of Call Service class does not have any parameter. Maybe I can I pass the Context object like parameter in some way?
Can you help me, please?
you could try answer from https://forums.xamarin.com/discussion/106938/context-is-obsolete-as-of-version-2-5
internal static MainActivity Instance { get; private set; }
protected override void OnCreate(Bundle bundle)
{
Instance = this;
// Forms initialization here...
}
//later where you need it:
var context = MainActivity.Instance;