Skip to main content

Overview

The social feed is the heart of Threadly, displaying posts from followed users, suggested content, and stories. The feed supports multiple post types including images and videos.

Home Fragment

The homeFragment manages the main feed interface:
fragments/homeFragment.java
public class homeFragment extends Fragment {
    ArrayList<Posts_Model> posts;
    ArrayList<Profile_Model_minimal> suggestUsersList;
    private ImagePostsFeedViewModel postsViewModel;
    StoriesViewModel storiesViewModel;
    MessagesViewModel messagesViewModel;
    InteractionNotificationViewModel notificationViewModel;
}

Feed Architecture

1

ViewModel Setup

ViewModels fetch data from the backend and expose LiveData for the UI to observe.
postsViewModel = new ViewModelProvider(requireActivity())
    .get(ImagePostsFeedViewModel.class);
storiesViewModel = new ViewModelProvider(requireActivity())
    .get(StoriesViewModel.class);
2

RecyclerView Configuration

Posts are displayed in a vertical RecyclerView with LinearLayoutManager.
posts = new ArrayList<>();
ImagePostsFeedAdapter postsFeedAdapter = 
    new ImagePostsFeedAdapter(requireActivity(), posts, suggestUsersList);
LinearLayoutManager postsLayoutManager = 
    new LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false);

mainXml.postsRecyclerView.setLayoutManager(postsLayoutManager);
mainXml.postsRecyclerView.setAdapter(postsFeedAdapter);
3

LiveData Observation

Observe changes from ViewModels and update UI accordingly.
postsViewModel.getPostsLiveData().observe(getViewLifecycleOwner(), posts_liveData -> {
    if (posts_liveData != null && !posts_liveData.isEmpty()) {
        posts.clear();
        posts.addAll(posts_liveData);
        postsFeedAdapter.notifyDataSetChanged();
        
        // Hide loading shimmer
        mainXml.shimmerView.stopShimmer();
        mainXml.shimmerView.setVisibility(View.GONE);
        mainXml.postsRecyclerView.setVisibility(View.VISIBLE);
    }
});

Post Types

Threadly supports multiple post types through a unified adapter:

Image Posts

Displayed using ImagePostsFeedAdapter with Glide for image loading:
adapters/postsAdapters/ImagePostsFeedAdapter.java
Glide.with(context)
    .load(Uri.parse(dataList.get(position).userDpUrl))
    .circleCrop()
    .placeholder(R.drawable.blank_profile)
    .into(holder.profile_img);

Video Posts

Video posts use ExoPlayer for smooth playback (see Reels for more details).

All-Type Feed Adapter

The AllTypePostFeedAdapter handles both images and videos:
adapters/postsAdapters/AllTypePostFeedAdapter.java
@Override
public int getItemViewType(int position) {
    return postModels.get(position).isVideo() ? 1 : TYPE_IMAGE;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    LayoutInflater inflater = LayoutInflater.from(context);
    View v;
    if (viewType == TYPE_IMAGE) {
        v = inflater.inflate(R.layout.image_post_reel_layout, parent, false);
        return new ImagePostViewHolder(v);
    } else {
        v = inflater.inflate(R.layout.reel_layout, parent, false);
        return new VideoPostViewHolder(v);
    }
}

Post Interactions

Like/Unlike

adapters/postsAdapters/AllTypePostFeedAdapter.java
holder.like_btn_image.setOnClickListener(v -> {
    if(!dataList.get(position).isliked){
        // Like the post
        holder.like_btn_image.setImageResource(R.drawable.red_heart_active_icon);
        holder.likes += 1.0;
        dataList.get(position).isliked = true;
        
        likeManager.likePost(dataList.get(position).postId, 
            new NetworkCallbackInterface() {
            @Override
            public void onSuccess() {
                // Like successful
            }
            
            @Override
            public void onError(String err) {
                // Revert on error
                holder.like_btn_image.setImageResource(R.drawable.heart_inactive);
                holder.likes -= 1.0;
                dataList.get(position).isliked = false;
            }
        });
    } else {
        // Unlike the post
        holder.like_btn_image.setImageResource(R.drawable.heart_inactive);
        holder.likes -= 1.0;
        dataList.get(position).isliked = false;
        
        likeManager.UnlikePost(dataList.get(position).postId, callback);
    }
});
The UI updates optimistically for better user experience. If the network request fails, the change is reverted.

Comments

Opens a bottom sheet dialog for viewing and adding comments:
holder.comment_btn_image.setOnClickListener(v -> 
    new PostCommentsViewerUtil(context)
        .setUpCommentDialog(dataList.get(position).postId)
);

Share

holder.share_icon_white.setOnClickListener(v -> {
    PostShareHelperUtil.OpenPostShareDialog(dataList.get(position), context);
});

Follow/Unfollow

holder.followBtn.setOnClickListener(v -> {
    holder.followBtn.setEnabled(false);
    holder.followBtn.setVisibility(View.GONE);
    
    followManager.follow(dataList.get(position).userId, 
        new NetworkCallbackInterface() {
        @Override
        public void onSuccess() {
            dataList.get(position).isFollowed = true;
            ReUsableFunctions.ShowToast("Following");
        }
        
        @Override
        public void onError(String err) {
            holder.followBtn.setVisibility(View.VISIBLE);
            holder.followBtn.setEnabled(true);
        }
    });
});

Stories Section

Stories appear at the top of the feed in a horizontal RecyclerView:
fragments/homeFragment.java
// Setup stories RecyclerView
LinearLayoutManager layoutManager = 
    new LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false);
StatusViewAdapter StoriesAdapter = new StatusViewAdapter(requireActivity(), 
    storiesData, 
    (userid, profilePic, list, position) -> 
        callback.openStoryOf(userid, profilePic, list, position)
);
mainXml.storyRecyclerView.setLayoutManager(layoutManager);
mainXml.storyRecyclerView.setAdapter(StoriesAdapter);

// Load stories
storiesViewModel.getStories().observe(getViewLifecycleOwner(), storiesModels -> {
    if(!storiesModels.isEmpty()){
        storiesData.clear();
        storiesData.addAll(storiesModels);
        StoriesAdapter.notifyDataSetChanged();
        mainXml.storiesShimmer.setVisibility(View.GONE);
        mainXml.storyRecyclerView.setVisibility(View.VISIBLE);
    }
});

My Story

Users can add their own story or view existing ones:
storiesViewModel.getMyStories().observe(getViewLifecycleOwner(), storyMediaModels -> {
    if(!storyMediaModels.isEmpty()){
        // User has active stories
        mainXml.StoryOuterBorderColor.setBackground(
            AppCompatResources.getDrawable(requireActivity(), R.drawable.red_circle)
        );
        mainXml.addStorySymbol.setVisibility(View.GONE);
        mainXml.MyStoryUsername.setText(R.string.your_story);
    } else {
        // No stories - show add button
        mainXml.addStorySymbol.setVisibility(View.VISIBLE);
    }
});

mainXml.myStoryLayoutMain.setOnClickListener(v -> {
    if(mainXml.addStorySymbol.getVisibility() == View.VISIBLE){
        // Add new story
        Intent intent = new Intent(requireActivity(), AddStoryActivity.class);
        intent.putExtra("title", "New Story");
        startActivity(intent);
    } else {
        // View my stories
        callback.openStoryOf(userid, profile, new ArrayList<>(), 0);
    }
});

Suggested Users

Suggested users appear within the feed to help users discover new accounts:
suggestUsersViewModel.getSuggestedUsers().observe(requireActivity(), 
    profileModelMinimals -> {
    suggestUsersList.clear();
    suggestUsersList.addAll(profileModelMinimals);
});

Pull to Refresh

Users can refresh the feed by pulling down:
mainXml.swipeRefresh.setOnRefreshListener(() -> {
    mainXml.swipeRefresh.setEnabled(false);
    postsViewModel.loadFeedPosts();
    suggestUsersViewModel.loadSuggestedUsers();
    storiesViewModel.loadStories();
    storiesViewModel.loadMyStories();
});

Top Bar Features

Unread Messages Badge

messagesViewModel.getUnreadConversationCunt(
    Core.getPreference().getString(SharedPreferencesKeys.UUID, "null")
).observe(getViewLifecycleOwner(), integer -> {
    if(integer > 0){
        mainXml.unreadMessageCounterLayout.setVisibility(View.VISIBLE);
        mainXml.unreadMessagesCounterText.setText(Integer.toString(integer));
    } else {
        mainXml.unreadMessageCounterLayout.setVisibility(View.GONE);
    }
});

Notification Indicator

notificationViewModel.getPendingNotificationCount()
    .observe(getViewLifecycleOwner(), integer -> {
    if(integer != null && integer > 0){
        mainXml.notificationDot.setVisibility(View.VISIBLE);
    } else {
        mainXml.notificationDot.setVisibility(View.GONE);
    }
});

Loading States

Shimmer Effect

While loading, a shimmer effect provides visual feedback:
if (posts_liveData != null && !posts_liveData.isEmpty()) {
    // Show content
    mainXml.shimmerView.stopShimmer();
    mainXml.shimmerView.setVisibility(View.GONE);
    mainXml.postsRecyclerView.setVisibility(View.VISIBLE);
} else {
    // Show loading
    mainXml.shimmerView.setVisibility(View.VISIBLE);
    mainXml.shimmerView.startShimmer();
}

Post Options Menu

Long-press or tap the options button to show a bottom sheet:
holder.optionDots_white.setOnClickListener(v -> {
    BottomSheetDialog OptionsDialog = 
        new BottomSheetDialog(context, R.style.TransparentBottomSheet);
    OptionsDialog.setContentView(R.layout.posts_action_options_layout);
    
    // Setup options: Download, Favorite, Follow/Unfollow, Report
    LinearLayout downloadBtnLayout = OptionsDialog.findViewById(R.id.download_btn);
    downloadBtnLayout.setOnClickListener(c -> {
        DownloadManagerUtil.downloadFromUri(context, 
            Uri.parse(dataList.get(position).postUrl));
        OptionsDialog.dismiss();
    });
    
    OptionsDialog.show();
});

Performance Optimizations

ViewHolder Pattern

RecyclerView ViewHolders are reused for smooth scrolling performance.

Image Caching

Glide automatically caches images to reduce network usage and load times.

Stable IDs

Adapter uses stable IDs for better animations and updates.

Nested Scrolling

Optimized nested scrolling for smooth feed navigation.