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
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 );
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);
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.
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 ();
}
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 ();
});
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.