My Flutter app has the following general structure:
The initial screen shows a list of contact objects. The user can tap a contact, which brings up
a detail screen that lets the user see the whole contact info, and modify any details temporarily. The user can either
dismiss the screen without saving the changes, or
tap a save button, which updates the contact permanently, and upon completion leads back to the initial screen.
I'm using FireStore. The list is built with Stream<QuerySnapshots>, and when the user taps an item, the app's router parses the route name (e.g. /contacts/123), creates the respective DocumentReference and forwards it to the detail screen's initializer, which then uses DocumentReference.get to load the details, and DocumentReference.updateData to save changes. Works beautifully – but is only a proof of concept.
Now I would like to hide FireStore and the remaining business logic behind a BLoC. This leads to some questions:
Keeping business logic out of the UI, as far as I understand it, implies that I should stick to named routes, and have the detail screen somehow use the route details to retrieve the relevant contact data through the BLoC. Is that true, and what's the best solution for this?
How can I subscribe to nested data using the BLoC? I want the detail screen to observe data changes, so that I can warn the user if the data becomes stale. Using functions on BLoCs (something like streamOfContact(contact) -> Stream<Contact>) is forbidden, so how would I do that with Sinks? Or is there generally a different way to do this without breaking the BLoC pattern? I'm very new to all of this, so may very well be I'm overlooking something important.
Similar question: How would I update a particular contact?
The tutorials I've found online only deal with root data (e.g. adding cart items to a cart, handling user authentication, ...), but I haven't seen an example showcasing nested data yet. Any help is highly appreciated!
1) Routing and navigation is in the responsibility of the UI layer. That means the UI layer must call Navigator.push[Named](...).
If it makes sense, you can move the logic that decides if the app should navigate to the detail screen:
// in the BLoC:
Stream<int> showContactDetail;
// in the UI layer:
bloc.showContactDetail.listen(_showContactDetail);
How you transfer the parameters to the detail route is totally up to you. You can use named routes, but it would be easier to transfer data with a builder:
void _showContactDetail(int contactId) {
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
return ContactDetailPage(
contactId: contactId,
);
}));
}
2) One BLoC should be tied to a single screen, that means there should be a separate BLoC for the detail screen (or dialog/sidebar/...), and you would pass the id of your contact to the second BLoC as a constructor parameter, or using a StreamSink, or using a simple setter method.
I would recommend you to use plain old methods instead of StreamSinks for the BLoC inputs. Here is a recent discussion about the topic.
3) The question is not only how to update your contact from the detail screen, but also how the detail BLoC obtains the contact data (if you are only transferring the contact id).
The answer: Another application layer, what I would call the data layer, that is shared between all BLoCs. You can store the data in Firebase, a sqlite database or a simple Map<int, Contact>.
The data layer would also propagate changes to the backend, and notify all BLoCs when the data has changed, probably using a Stream.
The next question that would come up is where you put this data layer (e.g. a class called ContactService):
You can create the ContactService in your ContactListPage, then pass it to the ContactDetailPage in a constructor parameter (using a route builder, as explained above). No magic here. A side effect that you may not want is that the service will be discarded when the list page is popped.
InheritedWidget that is above the MaterialApp, or at least above the Navigator generated by the MaterialApp (You can use the builder of MaterialApp to wrap the navigator with your own widgets). Putting it that high up in the tree ensures that all pages of your app can access it.
Using scoped_model, which is basically the same. Must also be inserted above the navigator to be accessible from both routes
A static variable/singleton
Related
I'm Use structures by default when possible,
what's the best practice to update the original struct instance a copy of it was passed to another variable.
let's say I have a Post struct
struct Post {
let title: String
let likes: Int
let viewsCount:Int
var comments:[Comment]
}
and we have a simple Master-Details Screens
when pushing the details screen we are copying the post to the details scene, and there the data may be changed as likes increments, added comments and so on,
so when pop This ViewController, to the new data model to update the original model in the Master ViewController.
what are the possible solution and the best practices for it?
assuming we are using MVC or MVP
You could create a common shared model class that is responsible for holding the Post data and that both view controllers could use and observe.
This really has nothing to do with structs; it is simply the usual issue of passing data into a view controller on creation and passing data back from that view controller on destruction. It happens that in your case you are positing that this is the "same" data — i.e. you pass a Post to the detail view controller and you pass a Post back from the detail view controller — but that is just a contingent fact.
So, to answer the question as formulated, you would use any of the usual techniques. The detail view controller would need to pass the modified Post back to the master view controller. It could use a Notification or (more directly) you could use the standard protocol-and-delegate architecture.
On the other hand, if Post is the basis of your app's data, it would not be unreasonable to argue that the premise itself is flawed: this should have been a class all along, not a struct, exactly so that the data can be maintained in a central place and references to it can be maintained in different places.
Indeed, if things are more complex, you might have the app's data (including the Post) live in some third location off in model-data-space, and have all view controllers send a notification up to the data when they change it and have the data send a notification down to all view controllers in response (that is what Ralf Ebert's answer quite reasonably suggests). That sort of thing is a lot easier nowadays because (in iOS 13) we have observable objects and the Combine framework.
I would create helper functions and make them mutating. e.g.
mutating func incrementLikes() {
likes += 1
}
Make sure your properties are var.
Ideally classes are best for the role you described as object maintain identity.
Each post has an identity.
According to apple docs "use classes when you need to control the identity of the data you're modeling"
When you share a class instance across your app, changes you make to that instance are visible to every part of your code that holds a reference to that instance. Use classes when you need your instances to have this kind of identity.
Classes vs strut
I'm sort of learning on the fly (coming from a desktop perspective), so forgive me if this seems dumb.
This would be a CRUD type of application, so imagine if you have two controllers for data entry:
SupervisorController
EmployeeController
Now, let's say the User goes to https://whatever/SupervisorController and before I do anything, I need to create some objects specific to the Supervisor screen (e.g. I create an Access object, which reads some data from a source, which tells me what they have access to on that specific Supervisor screen).
OK, in my Index method I construct that object and pass it to my view. But, that object won't persist when say, another method for that controller is called, or say Refresh on the browser is clicked (the constructor for the controller is called each time).
And let's say that the user goes to the browser and types https://whatever/SupervisorController/AnotherMethod themselves. Well, I only constructed that object in my Index Method. So now I have to construct that same object, again, there? And pass it to this view?
Where I'm getting at is that if someone goes to any method of the SupervisorController, if the object isn't there, I want to construct some "supervisor" specific objects, keep them available in my "private member variables", until I'm done with SupervisorController. Meaning, if they move on to say EmployeeController, those objects can now go away.
At EmployeeController, I'll want something similar. Complex objects created specifically for Employee CRUD, that I won't need once I'm done with Employees.
I mean, I really don't want to create these objects and store them in the Session for the lifetime of the session, as they are really only needed for their specific controllers. On top of that, how much data do I really want to be storing in session memory before it gets to be too much!
But if I don't store them somewhere, I would have to re-create these objects for every action/method. I don't want to create them in every method of a specific controller (I could be going to a database to get some specific information). For example, I'm playing around with a Grid Control on my Index page, and when you reach that page, there's the initial call to the Index method, and then the grid itself makes a call to this Read method I have. So I would be creating that object twice!? On one call to https://whatever/SupervisorController?
How do people handle what I'm trying to achieve? Or is there some way I need to look at it now, and implement it to those guidelines?
Dear SAPUI5 Developers,
I developed a SAPUI5 Fiori Worklist project by using WebIDE template projects.
In the Component.js file the OData model has been fetched.
var sServiceUrl = this.getMetadata().getManifestEntry("sap.app").dataSources.mainService.uri;
var oModel = new sap.ui.model.odata.ODataModel(sServiceUrl, {
json: true,
loadMetadataAsync: true
});
oModel.attachMetadataFailed(function() {
// Call some functions from APP controller to show suitable message
}, this);
this.setModel(oModel, "BrandSet");
This part of code causes a call to OData server to fetch data from the remote server.
Now I want to order the data in backend and then receive the data. Assume the sorting function has been implemented correctly in the backend.
Thus, if I use $orderby=name or $orderby=price it has to be sorted by name or price respectively.
In some toturial they said for ordering use sorter option inside of the XML view file. Like here:
https://sapui5.hana.ondemand.com/#docs/guide/c4b2a32bb72f483faa173e890e48d812.html
Now my questions are:
How to apply this sorting inside of the Component.js file where the Model is initiated?
The second question is how to apply this ordering when we apply a filter to the model? Like the example that in the following link applied filter:
https://sapui5.hana.ondemand.com/#docs/guide/5295470d7eee46c1898ee46c1b9ad763.html
In fact I am looking for a function or any kind of method that add the $orderby=xxx to the OData service call.
I found a way here: https://sapui5.hana.ondemand.com/docs/api/symbols/sap.ui.model.odata.ODataModel.html#constructor
If I use mParameters.serviceUrlParams then I can add some URL parameter to the service request but it has been said "these parameters will be attached to all requests". Does it mean if I add the $orderbywith this method then I can not get rid of that in the further requests on that data model for example for filtering?
An app would normally be structured a bit differently to what you propose. The general assumption is that there is a lot of data available from the backend and to load all this data at once can cause performance problems, particularly when used over a mobile phone network. Furthermore, the data is an oData Entity Set, that is, a list of many items of the same type, so the data would be presented in the UI with a list or table.
Typically the app would then show the data in some kind of list, such as sap.m.List or sap.m.Table. These controls are designed to work with large volumes of data and would load initially the first 20 items from the entity set. Only when the user scrolls down the list of data would additional items be loaded. Also, with these controls the user can decide to sort or filter the data according to certain fields in your data.
Assuming that your app is work like this, here is the standard approach.
The Main model (as defined in the manifest) would not be loaded in Component.js, but loaded via the binding defined in the xml views of the app. In the views you could define a fixed sort and/or filter in the binding or you could allow the user to set the sort and filter criteria. This would be handled programmatically in the respective controllers. Normally the changes that the user makes to the sort and filter would be applied separately. For example, he/she chooses an new sort order, the oData is reread and the new sort order shown in the UI. Then the user may chose a filter criteria, and this is applied too. Of course, in your programming logic in the controllers you would need to have applied any default sort and filter criteria and then maybe combine or replace these with the criteria selected by the user.
To see an example of this, I would suggest to look at the Template Application “SAP Fiori Master-Detail Application” in the WebIDE.
I'm trying to implement the Clean Architecture described by Robert Martin.
More specifically I'm using VIPER which is an iOS version of Clean Architecture.
The problem I have is as follows:
The user starts looking at a map with places (pins) on it.
If he clicks a button, a pin is dropped and he is taken to another view to create (or edit if it was a click to an existent pin) the place (or cancel).
In this other view, the user can edit the place's information and then click "back" or "done" (or "edit").
If he clicks "done", the PlaceDetailsViewController sends a message to the PlaceDetailsPresenter with the place information and the PlaceDetailsPresenter uses the CreatePlaceInteractor to create the place. This interactor returns the GUID which is used to identify the place.
If the user clicks back before creating the place, he gets back to the map and the dropped pin goes up and away (since it has no GUID, it is a new place and goes away).
If he clicks back after creating, the pin stays there (because it should have a GUID).
How should I connect all that and where should the place information (including GUID) be stored?
To clarify a little bit more:
Who should inform the MapPresenter that the pin stays there or goes away?
Is it the PlaceDetailsPresenter or should I pass this information to the PlaceDetailsWireframe -> MapWireframe -> MapPresenter -> MapView ?
Before going back, where should this GUID be stored, in the PlaceDetailsPresenter or in the PlaceDetailsViewController?
Right now that's what I have:
EDIT:
Basically I think the problem is that VIPER came from Robert Martin's Clean Architecture and he comes from a Web (Rails) background, so he doesn't think much about state (or don't specify it in his talks).
Which is mainly my question, where should the state be stored, how should the different modules communicate, should it be through the Wireframe, or through the database, or through the interactors, or through the Presenters communicating with each other like here https://github.com/objcio/issue-13-viper-swift.
I don't know much about Viper, so I can't comment about that. However, the gross state of the system should be held in the entity objects and manipulated by the interactors. The detailed state of the GUI (selection rectangles, etc) should be managed by a special connection between the controller and the presenter.
In your case there are two screens. The map, and the place editor. Clicking on the map causes either the placePinController to be invoked. It gathers the location of the click, and any other contextual data, constructs a placePinRequest data structure and passes it to the PlacePinInteractor which checks the location of the pin, validates it if necessary, create a Place entity to record the pin, constructs a EditPlaceReponse object and passes it to the EditPlacePresenter which brings up the place editor screen.
If the Done button is clicked on the place editor screen it invokes the EditPlaceController which gathers up the edited data into an EditPlaceRequest data structure and passes it to the EditPlaceInteractor. etc..
You specifically asked about the GUID of the pin. That would be created by the Place entity and passed back to the editPlacePresenter PlacePinInteractor.
In pure VIPER Router should hold modules input in form of a protocol. And modules Presenter should conform to it. So when Router uses other modules Router to assemble new module it passes it's input to it.
The second Router then assigns the input to its Presenters output. So basically Presenter of the first module becomes a delegate for the second modules Presenter.
So in your case when a user selects a place MapPresenter asks MapInteractor for a GUID and tells MapRouter to navigate to details for this GUID.
MapRouter asks PlaceDetailsRouter to assemble PlaceDetailsModule for this GUID and passes MapModuleInput to it. PlaceDetailsRouter assigns MapModuleInput to PlaceDetailsPresenter. PlaceDetailsRouter puts GUID in the PlaceDetailsInteractor
I am trying to have a single activity with a dynamically created fragment within its view.
I have a ActivityViewModel and a FragmentViewModel and matching views and layouts (ActivityView has a FrameLayout to host fragment). The fragment is shown by calling ShowViewModel<> from within ActivityViewModel.Start method.
I am using a CustomePresenter as described in http://enginecore.blogspot.ro/2013/06/more-dynamic-android-fragments-with.html.
It works fine from cold start and after resume. However, it won't work after activity is destroyed.
This is the sequence that happens in this problematic situation:
Activity is created, Mvx finds a cached ViewModel and attaches it to the Activity. Since ViewModel was cached it won't fire Start method (which triggers fragement creation). That's fine. But in next step Android recreates the fragment but it won't get its associated ViewModel because neither CustomPresenter (which takes care of that when fragment is created) or MvxFragment.OnCreate won't create it - like MvxActivity mechanism does. And thus I get a ViewModel-less fragment.
So I wonder, shouldn't be good if MvxFragemnt creates its own ViewModel upon create like MvxActivity does? Furthermore it should handle Save,Resume (call to adjacent ViewModel's methods).
Or perhaps I am handling this in wrong way or missing something.
I created a sample which describes the same problem, you are describing. You can alter the sample, to support multiple regions with multiple fragments in it. These regions can be used in presenter as well.
Please take a look at this presenter sample, which shows of a simple implementation of using fragments in an Android project: https://github.com/JelleDamen/CustomMvxAndroidPresenter
FYI:
I used the same tutorial as an inspiration. Let me know if you need any help with it.
Sorry, you are correct.
This behavior can be reproduced when creating a simple app with an activity and a fragment and then in the 'developer options' choose to always destroy activity. Now switch to another app and then switch back.
Init and Start are not called, the activity view-model is obtained from the cached view model.
This isn't related to fragments, it's about how view-model works for activity.
Now, regarding the fragment lifecycle and the fact that it doesn't get the view-model bound, as you mentioned, currently this is not available in Mvvmcross.