I'm hoping someone in the wide world of Android development has come across this problem/use case before, and has a relatively straight-forward approach for implementation.
I have a client Android application, that needs to render a list of fields that the end-user then fills out. Each field is a certain known-type, such as "Text" or "Radio", "MultiSelect", etc. When the user taps on a form, an API call is made to a backend which returns the schema for that form (ie: each field's UUID, title, description, hint text, etc), and the data for that form where some fields are likely already filled out from a prior time. Example of what data I would get over an API call:
{
"submittedBy": 8,
"updatedBy": 8,
"createdBy": 8,
"submittedDateMillis": 1489680600000,
"updatedDateMillis": 1489680600000,
"createdDateMillis": 1489680600000,
"name": "My Form",
"formTemplateId": 3,
"id": 0,
"schema": {
"6051c1e3-b4bf-4e6a-afe3-de2497dbff11": {
"units": "ft.",
"hintText": "Length of measurement",
"required": false,
"description": "Take the length of the measured item to 4 decimal places.",
"title": "Measurement",
"type": "number"
},
"fdf6ff0b-e60d-4591-a3e7-5467cd7bc67e": {
"enum": [
"Foo",
"Bar",
"Baz",
"Bat"
],
"required": true,
"hintText": "",
"description": "This is a description for a multiple choice question",
"title": "Multiple Choice (radio) title",
"type": "radio"
},
"203ef6d8-03fe-48e8-9a45-b18d12721d44": {
"enum": [
"Option 1",
"Option 2",
"Option 3",
"Option 4"
],
"required": true,
"hintText": "",
"description": "This is the description for a multiselect question",
"title": "This is the title for a multiselect question",
"type": "multiselect"
},
"751e9b8f-a59d-4e81-b3da-17ae44daa44e": {
"enum": [
"A dropdown answer",
"This is another option for a dropdown question it's limit is 130 characters"
],
"required": true,
"hintText": "",
"description": "This is the description for a dropdown question",
"title": "This is the title for a dropdown question",
"type": "select"
},
"33e13828-9171-4680-b68b-9838d4d42af8": {
"required": true,
"hintText": "This is the hint text for a text question limit 130 characters",
"description": "This is the description for a text question limit 5000 characters",
"title": "This is the title for a text question limit 130 characters",
"type": "text"
}
},
"fields": {
"6051c1e3-b4bf-4e6a-afe3-de2497dbff11": "5555.5555",
"fdf6ff0b-e60d-4591-a3e7-5467cd7bc67e": "Bar",
"751e9b8f-a59d-4e81-b3da-17ae44daa44e": "A dropdown answer",
"203ef6d8-03fe-48e8-9a45-b18d12721d44": [
"Option 1",
"Option 2",
"Option 4"
],
"33e13828-9171-4680-b68b-9838d4d42af8": "My answer for your text question."
}
}
The API call, say /api/v1/forms/0, returns the above data. In that I have schema which describes the field types, and fields which give me the answers to populate (some of which could be missing). They both have UUIDs which "match up", so I know what field data to put into what form field.
Now I have to render that Form, and allow the user to tap "Submit" and POST/PUT the new data back to the API.
What is an approach for dealing with this? I consider myself a beginner in Android, and from what I've come up with so far, is probably not the best solution (and probably doesn't scale beyond say, 50 questions, as the "render" and "submit" portions of this activity will become slow):
Make the API call, get the data (above example) back.
For every schema type, .inflate() an XML layout that is whatever that .type is (number, text, radio, etc), and construct a Java type (FormElement is what I'm calling it) that represents that schema JSON type. After .inflate(), .setTag(formElement) and "attach" that Java FormElement to it.
Get the widget inside that layout we just inflated, and if we have corresponding data from the fields mapping in the JSON, set the data to whatever that is. If not, leave it blank.
When the user taps "Submit", grab the Parent Form View, and get it's children. Loop through every child and pull out its FormElement via .getTag(). Get the FormElement#getType to find the type of the View, then work backwards and knowing the View we are iterating on, cast it, get it's inner data value, build the resulting JSON data back up, and PUT that to the API.
I might assign every Widget that represents a data entry point (Text, Radio, etc) a unique ID, based on the UUID from the schema (UUID is v1, so one way is to get the timestamp, and hope for the best, since we would be going from 128 bits to 32 bits). Then use this ID later, when I need to pull data out after the user taps Submit.
There looks to be some promising capability in Android's Data Binding Library, but I don't think Android's data binding can handle the "dynamic" nature of laying out this UI, with different Widgets that have different data types (some of these are Pull Down menus).
Is data binding a better approach here?
Can data binding handle both concerns of rendering the UI here, and helping fetch the data from that UI to ultimately compose my API PUT request back to the server?
Resources I've looked at so far, which shine some light on this overall problem:
http://www.mysamplecode.com/2011/10/android-dynamic-layout-using-xml-add.html for adding/inflating UI's based on pre-defined XML.
https://realm.io/news/data-binding-android-boyar-mount/ a really good tech talk by #george-mount that covers some basics of data binding.
.setTag() & .getTag() - What is the main purpose of setTag() getTag() methods of View? which seems to fit my use case, where I need to have the views "know" things about themselves (like where they came from in the JSON response).
Thank you all ahead of time!
You should create a representation of those fields in memory (like a Collection of Field's).
Then you can use RecyclerView to lay them out efficiently.
In RecyclerView, you can have view types (one for each Field type) that knows how to handle a particular field.
Inside the RecyclerView, to bind the views, you can use data binding. There is a demo on github that shows how to effectively use data binding with RecyclerView.
Last but not least, make sure your network operations are completely de-coupled from the UI. UI just reads the collection so each time you do a network request, it updates the collection, that notifies a change and RecyclerView will update itself. (you probably want to make optimistic updates on the collection since network requests may take time but that is a large topic to cover here).
Related
I get the following output from a request:
{
"allposts": [
{
"created": "2019-07-08T12:25:34.732217Z",
"description": "My First ImagePost",
"id": 1,
"imagepostdata": "http://127.0.0.1:8000/media/Images/None/placeholder.jpg",
"owner": "http://127.0.0.1:8000/users/getUserById/1/",
"profilePhotoOfUser": "http://127.0.0.1:8000/media/Images/None/placeholder.jpg",
"type": "ImagePost",
"url": "http://127.0.0.1:8000/posts/getImagePostById/1/"
},
{
"audio": "http://127.0.0.1:8000/media/Audios/None/placeholder.3gp",
"clique": "http://127.0.0.1:8000/cliques/getCliqueById/1/",
"created": "2019-07-08T12:25:56.748829Z",
"id": 2,
"image": "http://127.0.0.1:8000/media/Images/None/placeholder.jpg",
"owner": "http://127.0.0.1:8000/users/getUserById/1/",
"profilePhotoOfUser": "http://127.0.0.1:8000/media/Images/None/placeholder.jpg",
"text": "My First TextPost",
"type": "TextPost",
"url": "http://127.0.0.1:8000/posts/getTextPostById/2/",
"video": "http://127.0.0.1:8000/media/Videos/None/placeholder.mp4"
}
]
}
The first item in the JSON array represents an image post and the second item represents a text post.
I have image and text posts as post type. Here, you can see that the server gives the requesting client the different types collected as one output. The fields of the items can be different.
For ex.: imagepostdata vs. textpostdata.
Now, I am not sure how to define the model classes in my Android project. I use Retrofit as networking library combined with Gson.
My question: It is enough to write the ImagePost and TextPost model classes separately and let Retrofit/Gson handle the rest ?
Or should I copy/paste the output to http://www.jsonschema2pojo.org/ and get only one model class for the different items.
I am asking because in the callback methods for the Retrofit request, I have to provide also the model class to which the JSON data maps. And I did not know which one to choose.
What is the normal programming approach in such a case?
I have an upcoming video games app. A game release can come out on multiple platforms. I heard that firestore is much more flexible than firebase real time database on how you can retrieve your data. I'm stuck on how can I check if my game release documents in my release collection contains the user chosen platforms, so the app can show the games coming out on his platforms.
This is what I currently have
platforms is a list of Integer which contains platforms ids
databaseReference.collection(getRegionNode())
.whereEqualTo("m_y", monthFilter)
.whereArrayContains("platforms", platforms)
.orderBy("date", Query.Direction.ASCENDING).get().addOnCompleteListener(listener);
Here's an example of a game release document:
1369: {
"src": "Images/dead.png",
"name": "red dead 2",
"date": 2018-10-26,
"region": worldwide,
"platforms": "[12, 13, 54]"
}
Let's say for example, user wants to only be shown platform 12 and 13 games, I want a query that checks and retrieves all releases documents where 12 and 13 are in their platforms list. Thank you!
Firestore Query's whereArrayContains(String field, Object value):
Creates and returns a new Query with the additional filter that documents must contain the specified field, the value must be an array, and that the array must contain the provided value.
According to your comments, your platforms object that is passed as the second argument to this method is of type array. What you are actually doing, you are searching in the platforms property which is of type array for an array, which is not possible since the platforms array in your database contains numbers:
"platforms": "[12, 13, 54]"
And not arrays. A query like this:
databaseReference.collection(getRegionNode())
.whereEqualTo("m_y", monthFilter)
.whereArrayContains("platforms", 12) //Passed a number as the second argument
.orderBy("date", Query.Direction.ASCENDING).get().addOnCompleteListener(listener);
Will work fine because we are searching within the platforms array for a number. Please also note, if you intend to use this king of query, an index is required. For how to create an index, please see my answer from this post.
Even if you using the above query, you can filter your items using only one whereArrayContains() method call. If you will use more than one, the following error will occur:
Caused by: java.lang.IllegalArgumentException: Invalid Query. Queries only support having a single array-contains filter.
If you need to filter on more than one platform, you'll need to change the logic of structuring your database by creating a property for each individual platform that you have and chain whereEqualTo() method calls. I know it sounds a little weird but this is how Cloud Firestore works.
Your schema should like this:
1369: {
"src": "Images/dead.png",
"name": "red dead 2",
"date": 2018-10-26,
"region": worldwide,
"platformsOne": 12,
"platformsTwo": 13,
"platformsThree": 54
}
To find all the games for platform 12, 13 and 54, you should use a query that looks like this:
FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
Query query = rootRef.
.whereEqualTo("platformsOne", 12)
.whereEqualTo("platformsTwo", 13)
.whereEqualTo("platformsThree", 54);
{
"key1" : {
"region": 2,
"text": "This is text"
},
"key2" : {
"region": 8,
"text": "This is text"
}.
"key3" : {
"region": 6,
"text": "This is text"
}
}
I want to get the JSON objects with a region of 2 and 8.
Try this:
DatabaseReference ref=FirebaseDatabase.getInstance().getReference();
Query q=ref.orderByChild("region").startAt(2).endAt(8);
q.addValueEventListener(..){//..}
Assuming the the random keys are direct children to the root node.
If you want to use a range of regions, meaning all region from 2 to 8, in which will be also included all regions like (3, 4, 5, 6 and 7), Peter's answer will work perfectly fine.
But if you want to use only region 2 and 8, I recommend you duplicate data. This is a quite common practice when it comes to Firebase and is called denormalization and for that, I recommend you see this video, Denormalization is normal with the Firebase Database.
When you are duplicating data, there is one thing that need to keep in mind. In the same way you are adding data, you need to maintain it. With other words, if you want to update/detele an item, you need to do it in every place that it exists.
In your case, you should consider augmenting your data structure to allow a reverse lookup like this:
regionTwoAndEight
|
--- "key1": true
|
--- "key2": true
I'd like to identify users beyond their username. Is there any way to identify users with a token/code/ID# that is static and will never change?
Simply because usernames are flexible and can be changed at any time meaning I cannot keep track users.
Dani you can track users by their user id, which stays static even if they change their username.
In Instagram's API example they use this endpoint: https://api.instagram.com/v1/users/{user-id}/?access_token=ACCESS-TOKEN
which gives the following response:
{
"data": {
"id": "1574083",
"username": "snoopdogg",
"full_name": "Snoop Dogg",
"profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1574083_75sq_1295469061.jpg",
"bio": "This is my bio",
"website": "http://snoopdogg.com",
"counts": {
"media": 1320,
"follows": 420,
"followed_by": 3410
}
}
Depending on what data or which endpoints you are hitting beforehand, you should be able to get to the user ID and use that accordingly throughout your application.
I find Firebase Database sample very helpful, but I noticed something which worries me a little bit.
I mean in this example user can give star to the post, something like "Like it" on Facebook. In provided sample they nested stars into post, so we have sample object like this:
"post_id" : {
"author": "username",
"body": "Some content",
"starCount": 1
"stars" : {
"user_id_who_gave_star" : "true"
}
"title": "Some title",
"uid": "author_id"
}
Such solution has many advantages, like e.g. we can check if have already gave star and hide or change icon, or we can one transaction to change "starCount" and add next value to "stars".
But the problem is when we have big application and 1000 users gave star, so everytime when we download post data we download 1000 userIds which may be not best solution.
Question
My question is, what is best approach for such applications and have someone tested how Firebase works in this situation?
firebaser here
When writing the examples for our documentation, we always have to balance the need for having enough context, keeping the example small enough , and following our own best practices.
This is indeed one of the cases where we violate one of our own best practices "not nesting data". As you said: this means that a user downloading a post, gets all the UIDs of users that liked that post.
As the usage of the application scales, that may become a concern. If that is the case for your app, you should model the "users who upvoted a post" as a separate top-level node:
"posts": {
"post_id" : {
"author": "username",
"body": "Some content",
"starCount": 1
"title": "Some title",
"uid": "author_id"
}
},
"upvotes": {
"post_id" : {
"user_id_who_gave_star" : "true"
}
}