[2024]Digital Global Overview Report 2024 Meltwater.pdf
Creating a Facebook Clone - Part X - Transcript.pdf
1. Creating a Facebook Clone - Part X
There is a fine line between doing a mockup and implementing a data model. Once we have a UI in place designing the underlying data model becomes a puzzle of filling
in the missing pieces. Before we go into the newsfeed I'd like to create the objects that represent users and posts. Having these in place will make things easier when we
want to implement the server.
3. public class User implements PropertyBusinessObject {
public final Property<String, User> id = new Property<>("id");
public final Property<String, User>
firstName = new Property<>("firstName");
public final Property<String, User>
familyName = new Property<>("familyName");
public final Property<String, User> email = new Property<>("email");
public final Property<String, User> phone = new Property<>("phone");
public final Property<String, User> gender = new Property<>("gender");
public final LongProperty<User> birthday =
new LongProperty<>("birthday");
public final Property<String, User> avatar = new Property<>("avatar");
private final PropertyIndex idx = new PropertyIndex(this, "User",
id, firstName, familyName, email, phone, gender, avatar,
birthday);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
public String fullName() {
return firstName.get() + " " + familyName.get();
}
User
We also need a unique id, since security is crucial I will go with a string based unique id.
We also need a picture of the user to show next to his posts so an avatar entry will also help. This is all easy to express in a business object class.
These properties match the things we said we needed to know about a user
4. public class User implements PropertyBusinessObject {
public final Property<String, User> id = new Property<>("id");
public final Property<String, User>
firstName = new Property<>("firstName");
public final Property<String, User>
familyName = new Property<>("familyName");
public final Property<String, User> email = new Property<>("email");
public final Property<String, User> phone = new Property<>("phone");
public final Property<String, User> gender = new Property<>("gender");
public final LongProperty<User> birthday =
new LongProperty<>("birthday");
public final Property<String, User> avatar = new Property<>("avatar");
private final PropertyIndex idx = new PropertyIndex(this, "User",
id, firstName, familyName, email, phone, gender, avatar,
birthday);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
public String fullName() {
return firstName.get() + " " + familyName.get();
}
User
Dates are easy to store as long values since epoch
5. new LongProperty<>("birthday");
public final Property<String, User> avatar = new Property<>("avatar");
private final PropertyIndex idx = new PropertyIndex(this, "User",
id, firstName, familyName, email, phone, gender, avatar,
birthday);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
public String fullName() {
return firstName.get() + " " + familyName.get();
}
public Image getAvatar(float imageSize) {
String filename = "round-avatar-" + imageSize + "-" + id.get();
if(existsInStorage(filename)) {
try(InputStream is = createStorageInputStream(filename);) {
return Image.createImage(is);
} catch(IOException err) {
Log.e(err);
deleteStorageFile(filename);
}
}
int size = convertToPixels(imageSize);
Image temp = Image.createImage(size, size, 0xff000000);
Graphics g = temp.getGraphics();
User
Getting the full name is a pretty common thing so it makes sense to provide this helper method.
6. new LongProperty<>("birthday");
public final Property<String, User> avatar = new Property<>("avatar");
private final PropertyIndex idx = new PropertyIndex(this, "User",
id, firstName, familyName, email, phone, gender, avatar,
birthday);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
public String fullName() {
return firstName.get() + " " + familyName.get();
}
public Image getAvatar(float imageSize) {
String filename = "round-avatar-" + imageSize + "-" + id.get();
if(existsInStorage(filename)) {
try(InputStream is = createStorageInputStream(filename);) {
return Image.createImage(is);
} catch(IOException err) {
Log.e(err);
deleteStorageFile(filename);
}
}
int size = convertToPixels(imageSize);
Image temp = Image.createImage(size, size, 0xff000000);
Graphics g = temp.getGraphics();
User
Everything about this class is really simple except for the avatar. It should point at an image URL but how would we use it?
To simplify that I'll add a getAvatar method to User that will include the functionality of fetching/caching/resizing and shaping the avatar image.
The method accepts the image size in millimeters and returns an Image that matches that size
7. new LongProperty<>("birthday");
public final Property<String, User> avatar = new Property<>("avatar");
private final PropertyIndex idx = new PropertyIndex(this, "User",
id, firstName, familyName, email, phone, gender, avatar,
birthday);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
public String fullName() {
return firstName.get() + " " + familyName.get();
}
public Image getAvatar(float imageSize) {
String filename = "round-avatar-" + imageSize + "-" + id.get();
if(existsInStorage(filename)) {
try(InputStream is = createStorageInputStream(filename)) {
return Image.createImage(is);
} catch(IOException err) {
Log.e(err);
deleteStorageFile(filename);
}
}
int size = convertToPixels(imageSize);
Image temp = Image.createImage(size, size, 0xff000000);
Graphics g = temp.getGraphics();
User
If we have an image in cache for this user we'll return that image
8. new LongProperty<>("birthday");
public final Property<String, User> avatar = new Property<>("avatar");
private final PropertyIndex idx = new PropertyIndex(this, "User",
id, firstName, familyName, email, phone, gender, avatar,
birthday);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
public String fullName() {
return firstName.get() + " " + familyName.get();
}
public Image getAvatar(float imageSize) {
String filename = "round-avatar-" + imageSize + "-" + id.get();
if(existsInStorage(filename)) {
try(InputStream is = createStorageInputStream(filename)) {
return Image.createImage(is);
} catch(IOException err) {
Log.e(err);
deleteStorageFile(filename);
}
}
int size = convertToPixels(imageSize);
Image temp = Image.createImage(size, size, 0xff000000);
Graphics g = temp.getGraphics();
User
We load the image from storage, if loading failed due to corrupt image we'll delete the image file so the next call to this method will re-create it
9. if(existsInStorage(filename)) {
try(InputStream is = createStorageInputStream(filename)) {
return Image.createImage(is);
} catch(IOException err) {
Log.e(err);
deleteStorageFile(filename);
}
}
int size = convertToPixels(imageSize);
Image temp = Image.createImage(size, size, 0xff000000);
Graphics g = temp.getGraphics();
g.setAntiAliased(true);
g.setColor(0xffffff);
g.fillArc(0, 0, size, size, 0, 360);
Object mask = temp.createMask();
Style s = new Style();
s.setFgColor(0xc2c2c2);
s.setBgTransparency(255);
s.setBgColor(0xe9e9e9);
FontImage x = FontImage.createMaterial(
FontImage.MATERIAL_PERSON, s, size);
Image avatarImg = x.fill(size, size);
if(avatarImg instanceof FontImage) {
avatarImg = ((FontImage)avatarImg).toImage();
}
avatarImg = avatarImg.applyMask(mask);
User
We create a mask image, that's a white on black circle that we'll use to crop the image so it will appear circular
10. if(existsInStorage(filename)) {
try(InputStream is = createStorageInputStream(filename)) {
return Image.createImage(is);
} catch(IOException err) {
Log.e(err);
deleteStorageFile(filename);
}
}
int size = convertToPixels(imageSize);
Image temp = Image.createImage(size, size, 0xff000000);
Graphics g = temp.getGraphics();
g.setAntiAliased(true);
g.setColor(0xffffff);
g.fillArc(0, 0, size, size, 0, 360);
Object mask = temp.createMask();
Style s = new Style();
s.setFgColor(0xc2c2c2);
s.setBgTransparency(255);
s.setBgColor(0xe9e9e9);
FontImage x = FontImage.createMaterial(
FontImage.MATERIAL_PERSON, s, size);
Image avatarImg = x.fill(size, size);
if(avatarImg instanceof FontImage) {
avatarImg = ((FontImage)avatarImg).toImage();
}
avatarImg = avatarImg.applyMask(mask);
User
We use the material design font image of a person to create an avatar placeholder image
11. Graphics g = temp.getGraphics();
g.setAntiAliased(true);
g.setColor(0xffffff);
g.fillArc(0, 0, size, size, 0, 360);
Object mask = temp.createMask();
Style s = new Style();
s.setFgColor(0xc2c2c2);
s.setBgTransparency(255);
s.setBgColor(0xe9e9e9);
FontImage x = FontImage.createMaterial(
FontImage.MATERIAL_PERSON, s, size);
Image avatarImg = x.fill(size, size);
if(avatarImg instanceof FontImage) {
avatarImg = ((FontImage)avatarImg).toImage();
}
avatarImg = avatarImg.applyMask(mask);
if(avatar.get() != null) {
return URLImage.createToStorage(
EncodedImage.createFromImage(avatarImg, false),
filename, avatar.get(),
URLImage.createMaskAdapter(temp));
}
return avatarImg;
}
}
User
Masking doesn't work with FontImage so we convert the FontImage to a regular image and mask it
12. Graphics g = temp.getGraphics();
g.setAntiAliased(true);
g.setColor(0xffffff);
g.fillArc(0, 0, size, size, 0, 360);
Object mask = temp.createMask();
Style s = new Style();
s.setFgColor(0xc2c2c2);
s.setBgTransparency(255);
s.setBgColor(0xe9e9e9);
FontImage x = FontImage.createMaterial(
FontImage.MATERIAL_PERSON, s, size);
Image avatarImg = x.fill(size, size);
if(avatarImg instanceof FontImage) {
avatarImg = ((FontImage)avatarImg).toImage();
}
avatarImg = avatarImg.applyMask(mask);
if(avatar.get() != null) {
return URLImage.createToStorage(
EncodedImage.createFromImage(avatarImg, false),
filename, avatar.get(),
URLImage.createMaskAdapter(temp));
}
return avatarImg;
}
}
User
If we have an avatar URL we fetch the data from the URL into a file and mask using the given image automatically.
This isn't trivial but isn't hard either it will produce the round avatar images we see in Facebook. But we aren't done yet...
14. public class Post implements PropertyBusinessObject {
public final Property<String, Post> id = new Property<>("id");
public final Property<User, Post> user =
new Property<>("user", User.class);
public final LongProperty<Post> date = new LongProperty<>("date");
public final Property<String, Post> title = new Property<>("title");
public final Property<String, Post> content = new Property<>("content");
public final Property<String, Post> type = new Property<>("type");
public final Property<String, Post> visibility =
new Property<>("visibility");
public final Property<String, Post> styling = new Property<>("styling");
public final ListProperty<Comment, Post> comments =
new ListProperty<>("comment", Comment.class);
public final ListProperty<User, Post> likes =
new ListProperty<>("likes", User.class);
private final PropertyIndex idx = new PropertyIndex(this, "Post",
id, user, date, title, content, type, visibility, styling,
comments, likes);
@Override
public PropertyIndex getPropertyIndex() {
Post
This is the post class.
Again the class matches the UI almost to the letter
15. public class Post implements PropertyBusinessObject {
public final Property<String, Post> id = new Property<>("id");
public final Property<User, Post> user =
new Property<>("user", User.class);
public final LongProperty<Post> date = new LongProperty<>("date");
public final Property<String, Post> title = new Property<>("title");
public final Property<String, Post> content = new Property<>("content");
public final Property<String, Post> type = new Property<>("type");
public final Property<String, Post> visibility =
new Property<>("visibility");
public final Property<String, Post> styling = new Property<>("styling");
public final ListProperty<Comment, Post> comments =
new ListProperty<>("comment", Comment.class);
public final ListProperty<User, Post> likes =
new ListProperty<>("likes", User.class);
private final PropertyIndex idx = new PropertyIndex(this, "Post",
id, user, date, title, content, type, visibility, styling,
comments, likes);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
}
Post
Comments and likes use list property to handle multiple entries under a post
16. public class Comment implements PropertyBusinessObject {
public final Property<String, Comment> id = new Property<>("id");
public final Property<String, Comment> postId =
new Property<>("post");
public final Property<User, Comment> userId =
new Property<>("userId");
public final Property<String, Comment> parentComment =
new Property<>("parentComment");
public final LongProperty<Comment> date = new LongProperty<>("date");
public final Property<String, Comment> text = new Property<>("text");
private final PropertyIndex idx = new PropertyIndex(this, "Comment",
id, postId, userId, date, parentComment, text);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
}
Comment
This raises the obvious question about a comment, since we aren't close to implementing threaded comments yet I'll have to guess about the content of this entry.
Comments are bound to a parent post
17. public class Comment implements PropertyBusinessObject {
public final Property<String, Comment> id = new Property<>("id");
public final Property<String, Comment> postId =
new Property<>("post");
public final Property<User, Comment> userId =
new Property<>("userId");
public final Property<String, Comment> parentComment =
new Property<>("parentComment");
public final LongProperty<Comment> date = new LongProperty<>("date");
public final Property<String, Comment> text = new Property<>("text");
private final PropertyIndex idx = new PropertyIndex(this, "Comment",
id, postId, userId, date, parentComment, text);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
}
Comment
They can be posted by any user, notice I use ID's instead of objects. This will be simpler during parsing of the data
18. public class Comment implements PropertyBusinessObject {
public final Property<String, Comment> id = new Property<>("id");
public final Property<String, Comment> postId =
new Property<>("post");
public final Property<User, Comment> userId =
new Property<>("userId");
public final Property<String, Comment> parentComment =
new Property<>("parentComment");
public final LongProperty<Comment> date = new LongProperty<>("date");
public final Property<String, Comment> text = new Property<>("text");
private final PropertyIndex idx = new PropertyIndex(this, "Comment",
id, postId, userId, date, parentComment, text);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
}
Comment
The id of the parent comment or null. This allows us to implement nested comment threads.
We need one final piece in the data model for now...