Created
March 22, 2021 09:00
-
-
Save haerulmuttaqin/d1b6aa12ef12749ce2b1c4b4b46c5f6e to your computer and use it in GitHub Desktop.
#6 — Job Finder App (https://youtu.be/TpFe8WSCzBo)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<layout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools"> | |
<data> | |
<import type="android.view.View"/> | |
<variable | |
name="item" | |
type="id.haerulmuttaqin.jobfinder.data.entity.GithubJob" /> | |
<variable | |
name="state" | |
type="id.haerulmuttaqin.jobfinder.data.entity.NetworkState" /> | |
</data> | |
<androidx.coordinatorlayout.widget.CoordinatorLayout | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@color/blue_200"> | |
<com.google.android.material.appbar.AppBarLayout | |
android:id="@+id/appbar" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:background="@color/blue_200" | |
app:elevation="0dp"> | |
<androidx.appcompat.widget.Toolbar | |
android:id="@+id/toolbar" | |
android:layout_width="match_parent" | |
android:layout_height="?attr/actionBarSize" | |
android:theme="@style/Theme.JobFinder.Toolbar" | |
app:elevation="0dp" | |
app:layout_collapseMode="pin" | |
app:popupTheme="@style/Theme.JobFinder.PopupOverlay" | |
app:titleTextColor="@color/white" /> | |
</com.google.android.material.appbar.AppBarLayout> | |
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout | |
android:id="@+id/swipeRefresh" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@drawable/bg_rounded_top" | |
android:fillViewport="true" | |
android:fitsSystemWindows="true" | |
app:refreshing="@{state != null && state.getStatus().equals(state.Status.FIRST_LOADING) ? true : false}" | |
app:layout_anchor="@id/appbar" | |
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> | |
<RelativeLayout | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@drawable/bg_rounded_top" | |
android:fillViewport="true" | |
android:fitsSystemWindows="true" | |
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> | |
<ImageView | |
android:id="@+id/imgtop" | |
android:layout_width="40dp" | |
android:layout_height="5dp" | |
android:layout_centerHorizontal="true" | |
android:layout_marginTop="8dp" | |
android:background="@drawable/bg_strip_round" /> | |
<androidx.recyclerview.widget.RecyclerView | |
android:id="@+id/recyclerView" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="5dp" | |
android:layout_marginEnd="5dp" | |
android:layout_marginTop="15dp" | |
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> | |
<LinearLayout | |
android:id="@+id/emptyView" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical" | |
android:fillViewport="true" | |
android:clickable="false" | |
android:focusable="false" | |
android:visibility="@{state != null && state.getStatus().equals(state.Status.FIRST_FAILED) ? View.VISIBLE : View.INVISIBLE}" | |
app:layout_behavior="@string/appbar_scrolling_view_behavior" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent"> | |
<ImageView | |
android:layout_width="250dp" | |
android:layout_height="250dp" | |
android:layout_gravity="center" | |
android:layout_marginTop="100dp" | |
android:src="@drawable/ic_undraw_empty" /> | |
<TextView | |
android:id="@+id/textEmpty" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:gravity="center" | |
android:paddingStart="16dp" | |
android:paddingTop="16dp" | |
android:paddingEnd="16dp" | |
android:text="No Data Found" | |
android:fontFamily="@font/medium"/> | |
<TextView | |
android:id="@+id/textEmptyErr" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:gravity="center" | |
android:paddingStart="16dp" | |
android:paddingBottom="16dp" | |
android:paddingEnd="16dp" | |
android:text="@{state.msg}" | |
android:fontFamily="@font/book"/> | |
</LinearLayout> | |
<com.facebook.shimmer.ShimmerFrameLayout | |
android:id="@+id/shimmer" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical" | |
android:layout_marginTop="16dp" | |
android:visibility="@{state != null && state.getStatus().equals(state.Status.FIRST_LOADING) ? View.VISIBLE : View.INVISIBLE}" | |
app:layout_behavior="@string/appbar_scrolling_view_behavior" | |
app:shimmer_duration="800" | |
app:shimmer_auto_start="true"> | |
<LinearLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:orientation="vertical"> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
</LinearLayout> | |
</com.facebook.shimmer.ShimmerFrameLayout> | |
<ImageView | |
android:id="@+id/shadow" | |
android:layout_width="match_parent" | |
android:layout_height="36dp" | |
android:layout_marginStart="3dp" | |
android:layout_marginEnd="3dp" | |
android:layout_marginTop="15dp" | |
android:src="@drawable/bg_shadow_up_to_down" | |
app:layout_anchor="@+id/scroll" | |
app:layout_anchorGravity="top|center" /> | |
</RelativeLayout> | |
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> | |
</androidx.coordinatorlayout.widget.CoordinatorLayout> | |
</layout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<layout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto"> | |
<data> | |
<variable | |
name="viewModel" | |
type="id.haerulmuttaqin.jobfinder.ui.MainViewModel" /> | |
</data> | |
<androidx.coordinatorlayout.widget.CoordinatorLayout | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@color/blue_200"> | |
<com.google.android.material.appbar.AppBarLayout | |
android:id="@+id/appbar" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:elevation="0dp"> | |
<com.google.android.material.appbar.CollapsingToolbarLayout | |
android:id="@+id/collapse" | |
android:layout_width="match_parent" | |
android:layout_height="280dp" | |
app:collapsedTitleTextAppearance="@style/collapsedTitleStyle" | |
app:contentScrim="?attr/colorPrimary" | |
app:expandedTitleMarginBottom="26dp" | |
app:expandedTitleMarginStart="26dp" | |
app:expandedTitleTextAppearance="@style/expandedTitleStyle" | |
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" | |
app:title="@string/app_name" | |
app:titleEnabled="true"> | |
<RelativeLayout | |
app:layout_collapseMode="parallax" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content"> | |
<ImageView | |
android:id="@+id/banner" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:scaleType="centerCrop" | |
android:src="@drawable/banner_1" /> | |
<TextView | |
android:id="@+id/welcomeText" | |
android:text="Hi, Welcome back" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="26dp" | |
android:layout_marginTop="40dp" | |
android:layout_marginEnd="10dp" | |
android:fontFamily="@font/book" | |
android:textSize="16sp" | |
android:textColor="@color/white" | |
android:layout_toStartOf="@id/notif"/> | |
<TextView | |
android:id="@+id/secondaryText" | |
android:text="Find your perfect job" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_below="@id/welcomeText" | |
android:layout_marginStart="26dp" | |
android:layout_marginTop="2dp" | |
android:layout_marginEnd="10dp" | |
android:layout_toStartOf="@id/notif" | |
android:textSize="26sp" | |
android:textColor="@color/white" | |
android:fontFamily="@font/bold" /> | |
<ImageView | |
android:id="@+id/notif" | |
android:layout_width="26dp" | |
android:layout_height="40dp" | |
android:layout_alignParentTop="true" | |
android:layout_alignParentEnd="true" | |
android:layout_marginTop="42dp" | |
android:layout_marginEnd="16dp" | |
android:contentDescription="@string/app_name" | |
android:src="@drawable/ic_baseline_more_vert_24" | |
android:paddingBottom="10dp" | |
app:tint="@color/white"/> | |
<LinearLayout | |
android:id="@+id/searchLayout" | |
android:orientation="vertical" | |
android:layout_alignParentBottom="true" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content"> | |
<com.google.android.material.card.MaterialCardView | |
android:id="@+id/card_search" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginLeft="20dp" | |
android:layout_marginRight="20dp" | |
android:layout_marginBottom="60dp" | |
android:clickable="true" | |
android:focusable="true" | |
android:background="?attr/selectableItemBackground" | |
app:strokeWidth="0.3dp" | |
app:cardCornerRadius="20dp" | |
app:cardElevation="0dp" | |
app:strokeColor="@color/blue_300" | |
app:cardBackgroundColor="@color/blue_250"> | |
<RelativeLayout | |
android:layout_width="match_parent" | |
android:layout_height="60dp"> | |
<TextView | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:text="What are you looking for?" | |
android:singleLine="true" | |
android:layout_centerVertical="true" | |
android:layout_marginStart="16dp" | |
android:layout_marginEnd="16dp" | |
android:textColor="@color/blue_050" | |
android:fontFamily="@font/book" | |
android:textSize="16dp"/> | |
<ImageView | |
android:src="@drawable/ic_round_search_24" | |
android:layout_width="25dp" | |
android:layout_height="60dp" | |
android:layout_alignParentEnd="true" | |
android:layout_marginEnd="16dp" | |
app:tint="@color/blue_050"/> | |
</RelativeLayout> | |
</com.google.android.material.card.MaterialCardView> | |
</LinearLayout> | |
</RelativeLayout> | |
<androidx.appcompat.widget.Toolbar | |
android:id="@+id/toolbar" | |
android:layout_width="match_parent" | |
android:layout_height="?attr/actionBarSize" | |
android:theme="@style/Theme.JobFinder.Toolbar" | |
android:fitsSystemWindows="true" | |
app:background="@android:color/transparent" | |
app:layout_collapseMode="pin" | |
app:popupTheme="@style/Theme.JobFinder.PopupOverlay" | |
app:titleTextColor="@color/white" /> | |
</com.google.android.material.appbar.CollapsingToolbarLayout> | |
</com.google.android.material.appbar.AppBarLayout> | |
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout | |
android:id="@+id/swipeRefresh" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@drawable/bg_rounded_top" | |
android:fillViewport="true" | |
android:fitsSystemWindows="true" | |
app:layout_anchor="@id/appbar" | |
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> | |
<RelativeLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content"> | |
<androidx.core.widget.NestedScrollView | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginTop="16dp" | |
android:fillViewport="true"> | |
<RelativeLayout | |
android:id="@+id/contentLayout" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content"> | |
<TextView | |
android:id="@+id/markedTitle" | |
android:text="Marked Job" | |
android:textSize="16dp" | |
android:textColor="@color/gray_600" | |
android:layout_marginStart="21dp" | |
android:layout_marginEnd="21dp" | |
android:layout_marginTop="25dp" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:fontFamily="@font/medium"/> | |
<TextView | |
android:id="@+id/showAllMarked" | |
android:clickable="true" | |
android:focusable="true" | |
android:background="?attr/selectableItemBackgroundBorderless" | |
android:text="Show all" | |
android:textSize="14dp" | |
android:textColor="@color/gray_300" | |
android:layout_marginStart="21dp" | |
android:layout_marginEnd="21dp" | |
android:layout_marginTop="25dp" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_alignParentEnd="true" | |
android:fontFamily="@font/book"/> | |
<androidx.recyclerview.widget.RecyclerView | |
android:id="@+id/recyclerViewMarked" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginTop="6dp" | |
android:layout_marginBottom="10dp" | |
android:layout_below="@id/markedTitle" | |
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" | |
android:nestedScrollingEnabled="true" | |
android:orientation="horizontal"/> | |
<TextView | |
android:id="@+id/recommendedTitle" | |
android:text="Recommended Job" | |
android:textSize="16dp" | |
android:textColor="@color/gray_600" | |
android:layout_marginStart="21dp" | |
android:layout_marginEnd="21dp" | |
android:layout_marginTop="25dp" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_below="@id/recyclerViewMarked" | |
android:fontFamily="@font/medium"/> | |
<TextView | |
android:id="@+id/showAllRecommended" | |
android:clickable="true" | |
android:focusable="true" | |
android:background="?attr/selectableItemBackgroundBorderless" | |
android:text="Show all" | |
android:textSize="14dp" | |
android:textColor="@color/gray_300" | |
android:layout_marginStart="21dp" | |
android:layout_marginEnd="21dp" | |
android:layout_marginTop="25dp" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_alignParentEnd="true" | |
android:layout_below="@id/recyclerViewMarked" | |
android:fontFamily="@font/book"/> | |
<androidx.recyclerview.widget.RecyclerView | |
android:id="@+id/recyclerView" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:layout_marginStart="5dp" | |
android:layout_marginEnd="5dp" | |
android:layout_below="@id/recommendedTitle" | |
android:layout_marginBottom="20dp" | |
app:layout_behavior="@string/appbar_scrolling_view_behavior" | |
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> | |
</RelativeLayout> | |
</androidx.core.widget.NestedScrollView> | |
<com.facebook.shimmer.ShimmerFrameLayout | |
android:id="@+id/shimmer" | |
android:visibility="gone" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical" | |
android:layout_marginTop="16dp" | |
app:layout_behavior="@string/appbar_scrolling_view_behavior" | |
app:shimmer_duration="800" | |
app:shimmer_auto_start="true"> | |
<LinearLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:orientation="vertical"> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
<include layout="@layout/item_job_placeholder" /> | |
</LinearLayout> | |
</com.facebook.shimmer.ShimmerFrameLayout> | |
<LinearLayout | |
android:id="@+id/emptyView" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical" | |
android:fillViewport="true" | |
android:clickable="false" | |
android:focusable="false" | |
app:layout_behavior="@string/appbar_scrolling_view_behavior" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent"> | |
<ImageView | |
android:layout_width="250dp" | |
android:layout_height="250dp" | |
android:layout_gravity="center" | |
android:layout_marginTop="100dp" | |
android:src="@drawable/ic_undraw_empty" /> | |
<TextView | |
android:id="@+id/textEmpty" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:gravity="center" | |
android:paddingStart="16dp" | |
android:paddingTop="16dp" | |
android:paddingEnd="16dp" | |
android:text="No Data Found" | |
android:fontFamily="@font/medium"/> | |
<TextView | |
android:id="@+id/textEmptyErr" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:gravity="center" | |
android:paddingStart="16dp" | |
android:paddingBottom="16dp" | |
android:paddingEnd="16dp" | |
android:fontFamily="@font/book"/> | |
</LinearLayout> | |
<ImageView | |
android:id="@+id/shadow" | |
android:layout_width="match_parent" | |
android:layout_height="35dp" | |
android:layout_marginTop="16dp" | |
android:src="@drawable/bg_shadow_up_to_down" | |
android:visibility="visible" /> | |
<ImageView | |
android:background="@drawable/bg_strip_round" | |
android:layout_width="40dp" | |
android:layout_height="5dp" | |
android:layout_marginTop="8dp" | |
android:layout_centerHorizontal="true"/> | |
</RelativeLayout> | |
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> | |
</androidx.coordinatorlayout.widget.CoordinatorLayout> | |
</layout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.List; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import retrofit2.Call; | |
import retrofit2.http.GET; | |
import retrofit2.http.Query; | |
public interface ApiInterface { | |
@GET("positions.json") | |
Call<List<GithubJob>> getJobList(); | |
@GET("positions.json") | |
Call<List<GithubJob>> getJobList(@Query("page") int page); | |
@GET("positions.json") | |
Call<List<GithubJob>> searchJobList(@Query("search") String keyword); | |
@GET("positions.json") | |
Call<List<GithubJob>> searchJobList(@Query("page") int page, @Query("search") String keyword); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.List; | |
import javax.inject.Inject; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import retrofit2.Call; | |
public class ConnectionServer { | |
@Inject | |
ApiInterface apiInterface; | |
public ConnectionServer(ApiInterface apiInterface) { | |
this.apiInterface = apiInterface; | |
} | |
public Call<List<GithubJob>> getJobList() { | |
return apiInterface.getJobList(); | |
} | |
public Call<List<GithubJob>> getJobListByPage(int page) { | |
return apiInterface.getJobList(page); | |
} | |
public Call<List<GithubJob>> searchJobList(String keyword) { | |
return apiInterface.searchJobList(keyword); | |
} | |
public Call<List<GithubJob>> searchJobListByPage(int page, String keyword) { | |
return apiInterface.searchJobList(page, keyword); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.annotation.NonNull; | |
import androidx.lifecycle.MutableLiveData; | |
import androidx.paging.DataSource; | |
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository; | |
import io.reactivex.subjects.ReplaySubject; | |
public class GithubDataSourceFactory extends DataSource.Factory { | |
private MutableLiveData<GithubPageKeyedDataSource> networkStatus; | |
private GithubPageKeyedDataSource githubPageKeyedDataSource; | |
public GithubDataSourceFactory(ConnectionServer connectionServer, GithubJobRepository repository, String keyword) { | |
this.networkStatus = new MutableLiveData<>(); | |
githubPageKeyedDataSource = new GithubPageKeyedDataSource(connectionServer, repository, keyword); | |
} | |
@NonNull | |
@Override | |
public DataSource create() { | |
networkStatus.postValue(githubPageKeyedDataSource); | |
return githubPageKeyedDataSource; | |
} | |
public MutableLiveData<GithubPageKeyedDataSource> getNetworkStatus() { | |
return networkStatus; | |
} | |
public ReplaySubject<GithubJob> getData() { | |
return githubPageKeyedDataSource.getGithubJobObserve(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.annotation.NonNull; | |
import androidx.recyclerview.widget.DiffUtil; | |
import androidx.room.Entity; | |
import androidx.room.PrimaryKey; | |
import com.google.gson.annotations.Expose; | |
import com.google.gson.annotations.SerializedName; | |
import java.io.Serializable; | |
@Entity | |
public class GithubJob implements Serializable { | |
@PrimaryKey | |
@SerializedName("id") | |
@Expose | |
@NonNull | |
public String id; | |
@SerializedName("type") | |
@Expose | |
public String type; | |
@SerializedName("url") | |
@Expose | |
public String url; | |
@SerializedName("created_at") | |
@Expose | |
public String createdAt; | |
@SerializedName("company") | |
@Expose | |
public String company; | |
@SerializedName("company_url") | |
@Expose | |
public String companyUrl; | |
@SerializedName("location") | |
@Expose | |
public String location; | |
@SerializedName("title") | |
@Expose | |
public String title; | |
@SerializedName("description") | |
@Expose | |
public String description; | |
@SerializedName("how_to_apply") | |
@Expose | |
public String howToApply; | |
@SerializedName("company_logo") | |
@Expose | |
public String companyLogo; | |
public int is_mark; | |
public static DiffUtil.ItemCallback<GithubJob> DIFF_CALLBACK = new DiffUtil.ItemCallback<GithubJob>() { | |
@Override | |
public boolean areItemsTheSame(@NonNull GithubJob oldItem, @NonNull GithubJob newItem) { | |
return oldItem.id.equals(newItem.id); | |
} | |
@Override | |
public boolean areContentsTheSame(@NonNull GithubJob oldItem, @NonNull GithubJob newItem) { | |
return oldItem.id.equals(newItem.id); | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.lifecycle.LiveData; | |
import androidx.room.Dao; | |
import androidx.room.Insert; | |
import androidx.room.OnConflictStrategy; | |
import androidx.room.Query; | |
import java.util.List; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
@Dao | |
public interface GithubJobDao { | |
@Insert(onConflict = OnConflictStrategy.REPLACE) | |
void insert(GithubJob githubJob); | |
@Query("SELECT * FROM githubjob ORDER BY createdAt DESC LIMIT 10") | |
LiveData<List<GithubJob>> getLiveData(); | |
@Query("SELECT * FROM githubjob ORDER BY createdAt DESC") | |
List<GithubJob> getList(); | |
@Query("SELECT * FROM githubjob WHERE LOWER(title) LIKE :keyword OR LOWER(description) LIKE :keyword ORDER BY createdAt DESC") | |
LiveData<List<GithubJob>> searchLiveData(String keyword); | |
@Query("SELECT * FROM githubjob WHERE LOWER(title) LIKE :keyword OR LOWER(description) LIKE :keyword ORDER BY createdAt DESC") | |
List<GithubJob> searchList(String keyword); | |
@Query("SELECT * FROM githubjob where is_mark = 1 ORDER BY createdAt DESC") | |
LiveData<List<GithubJob>> getLiveDataMarked(); | |
@Query("SELECT * FROM githubjob where id = :id") | |
GithubJob getDataById(String id); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context; | |
import androidx.lifecycle.LiveData; | |
import androidx.lifecycle.MediatorLiveData; | |
import androidx.paging.LivePagedListBuilder; | |
import androidx.paging.PagedList; | |
import java.util.List; | |
import java.util.concurrent.Executor; | |
import java.util.concurrent.Executors; | |
import id.haerulmuttaqin.jobfinder.Utils; | |
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.entity.NetworkState; | |
import id.haerulmuttaqin.jobfinder.data.paging.GithubDataSourceFactory; | |
import id.haerulmuttaqin.jobfinder.data.paging.GithubNetwork; | |
import id.haerulmuttaqin.jobfinder.data.paging.LocalDataSourceFactory; | |
import io.reactivex.schedulers.Schedulers; | |
public class GithubJobRepository { | |
private static GithubJobDatabase database; | |
private static GithubJobRepository repository; | |
private static GithubNetwork network; | |
private MediatorLiveData githubMediatorLiveData; | |
private LiveData<PagedList<GithubJob>> pagedListLiveData; | |
public GithubJobRepository(Context context) { | |
database = GithubJobDatabase.getDatabase(context); | |
} | |
public static GithubJobRepository getInstance(Context context) { | |
if (repository == null) { | |
repository = new GithubJobRepository(context); | |
} | |
return repository; | |
} | |
public void updateMarkJob(GithubJob githubJob) { | |
githubJob.is_mark = githubJob.is_mark == 1 ? 0 : 1; | |
database.githubJobDao().insert(githubJob); | |
} | |
public void insert(GithubJob githubJob) { | |
if (database.githubJobDao().getDataById(githubJob.id) == null) { | |
database.githubJobDao().insert(githubJob); | |
} | |
} | |
public GithubJob getById(String id) { | |
return database.githubJobDao().getDataById(id); | |
} | |
public LiveData<List<GithubJob>> getLiveData() { | |
return database.githubJobDao().getLiveData(); | |
} | |
public LiveData<List<GithubJob>> searchLiveData(String keyword) { | |
keyword = "%" + keyword + "%"; | |
return database.githubJobDao().searchLiveData(keyword); | |
} | |
public LiveData<List<GithubJob>> getLiveDataMarked() { | |
return database.githubJobDao().getLiveDataMarked(); | |
} | |
/*PAGING*/ | |
public LiveData<PagedList<GithubJob>> getPagedListLiveData() { | |
return pagedListLiveData; | |
} | |
private PagedList.BoundaryCallback<GithubJob> boundaryCallback = new PagedList.BoundaryCallback<GithubJob>() { | |
@Override | |
public void onZeroItemsLoaded() { | |
super.onZeroItemsLoaded(); | |
githubMediatorLiveData.addSource(getPagedListLiveData(), value -> { | |
githubMediatorLiveData.setValue(value); | |
githubMediatorLiveData.removeSource(getPagedListLiveData()); | |
}); | |
} | |
}; | |
public void initPageDao(String keyword) { | |
PagedList.Config config = (new PagedList.Config.Builder()).setEnablePlaceholders(false) | |
.setInitialLoadSizeHint(Integer.MAX_VALUE).setPageSize(Integer.MAX_VALUE).build(); | |
Executor executor = Executors.newFixedThreadPool(3); | |
LocalDataSourceFactory localDataSourceFactory = new LocalDataSourceFactory(database.githubJobDao(), keyword); | |
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(localDataSourceFactory, config); | |
pagedListLiveData = livePagedListBuilder.setFetchExecutor(executor).build(); | |
} | |
public LiveData<PagedList<GithubJob>> getDataByPage(ConnectionServer connectionServer, GithubJobRepository repository, String keyword) { | |
initPageDao(keyword); | |
GithubDataSourceFactory githubDataSourceFactory = new GithubDataSourceFactory(connectionServer, repository, keyword); | |
network = new GithubNetwork(githubDataSourceFactory, boundaryCallback); | |
githubMediatorLiveData = new MediatorLiveData<>(); | |
githubMediatorLiveData.addSource(network.getPagedListLiveData(), value -> { | |
githubMediatorLiveData.setValue(value); | |
}); | |
githubDataSourceFactory.getData() | |
.observeOn(Schedulers.io()) | |
.subscribe(item -> { | |
item.createdAt = Utils.dateFormatter(item.createdAt); | |
database.githubJobDao().insert(item); | |
}); | |
return githubMediatorLiveData; | |
} | |
public LiveData<NetworkState> getNetworkState() { | |
return network.getNetworkStateLiveData(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.arch.core.util.Function; | |
import androidx.lifecycle.LiveData; | |
import androidx.lifecycle.Transformations; | |
import androidx.paging.LivePagedListBuilder; | |
import androidx.paging.PagedList; | |
import java.util.concurrent.Executor; | |
import java.util.concurrent.Executors; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.entity.NetworkState; | |
public class GithubNetwork { | |
final private LiveData<PagedList<GithubJob>> pagedListLiveData; | |
final private LiveData<NetworkState> networkStateLiveData; | |
public GithubNetwork(GithubDataSourceFactory githubDataSourceFactory, PagedList.BoundaryCallback<GithubJob> boundaryCallback) { | |
PagedList.Config config = (new PagedList.Config.Builder()) | |
.setEnablePlaceholders(false) | |
.setInitialLoadSizeHint(1) | |
.setPageSize(50) | |
.build(); | |
this.networkStateLiveData = Transformations.switchMap( | |
githubDataSourceFactory.getNetworkStatus(), | |
(Function<GithubPageKeyedDataSource, LiveData<NetworkState>>) GithubPageKeyedDataSource::getNetworkState); | |
Executor executor = Executors.newFixedThreadPool(3); | |
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(githubDataSourceFactory, config); | |
this.pagedListLiveData = livePagedListBuilder.setFetchExecutor(executor).setBoundaryCallback(boundaryCallback).build(); | |
} | |
public LiveData<NetworkState> getNetworkStateLiveData() { | |
return networkStateLiveData; | |
} | |
public LiveData<PagedList<GithubJob>> getPagedListLiveData() { | |
return pagedListLiveData; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.annotation.NonNull; | |
import androidx.lifecycle.MutableLiveData; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.concurrent.atomic.AtomicInteger; | |
import id.haerulmuttaqin.jobfinder.Utils; | |
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.entity.NetworkState; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository; | |
import io.reactivex.subjects.ReplaySubject; | |
import retrofit2.Call; | |
import retrofit2.Callback; | |
import retrofit2.Response; | |
public class GithubPageKeyedDataSource extends androidx.paging.PageKeyedDataSource<String, GithubJob> { | |
private final MutableLiveData networkState; | |
private final ReplaySubject<GithubJob> githubJobObserve; | |
private final ConnectionServer connectionServer; | |
private final GithubJobRepository repository; | |
private final String keyword; | |
public GithubPageKeyedDataSource(ConnectionServer connectionServer, GithubJobRepository repository, String keyword) { | |
this.networkState = new MutableLiveData(); | |
this.connectionServer = connectionServer; | |
this.repository = repository; | |
this.githubJobObserve = ReplaySubject.create(); | |
this.keyword = keyword; | |
} | |
public MutableLiveData getNetworkState() { | |
return networkState; | |
} | |
public ReplaySubject<GithubJob> getGithubJobObserve() { | |
return githubJobObserve; | |
} | |
@Override | |
public void loadInitial(@NonNull LoadInitialParams<String> params, @NonNull LoadInitialCallback<String, GithubJob> callback) { | |
networkState.postValue(NetworkState.FIRST_LOADING); | |
Call<List<GithubJob>> call = null; | |
if (keyword != null) { | |
call = connectionServer.searchJobListByPage(1, keyword); | |
} else { | |
call = connectionServer.getJobListByPage(1); | |
} | |
call.enqueue(new Callback<List<GithubJob>>() { | |
@Override | |
public void onResponse(Call<List<GithubJob>> call, Response<List<GithubJob>> response) { | |
if (response.isSuccessful()) { | |
networkState.postValue(NetworkState.LOADED); | |
callback.onResult(response.body(), Integer.toString(1), Integer.toString(2)); | |
if (response.body().size() > 0) { | |
for (GithubJob item : response.body()) { | |
githubJobObserve.onNext(item); | |
} | |
} else { | |
networkState.postValue(new NetworkState(NetworkState.Status.FIRST_FAILED, "No data found")); | |
} | |
} else { | |
networkState.postValue(new NetworkState(NetworkState.Status.FIRST_FAILED, response.message())); | |
} | |
} | |
@Override | |
public void onFailure(Call<List<GithubJob>> call, Throwable t) { | |
networkState.postValue(new NetworkState(NetworkState.Status.FIRST_FAILED, Utils.errorMessageHandler(call, t))); | |
callback.onResult(new ArrayList<>(), Integer.toString(1), Integer.toString(2)); | |
} | |
}); | |
} | |
@Override | |
public void loadBefore(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, GithubJob> callback) {} | |
@Override | |
public void loadAfter(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, GithubJob> callback) { | |
networkState.postValue(NetworkState.LOADING); | |
final AtomicInteger page = new AtomicInteger(0); | |
try { | |
page.set(Integer.parseInt(params.key)); | |
} catch (NumberFormatException e) { | |
e.printStackTrace(); | |
} | |
Call<List<GithubJob>> call = null; | |
if (keyword != null) { | |
call = connectionServer.searchJobListByPage(page.get(), keyword); | |
} else { | |
call = connectionServer.getJobListByPage(page.get()); | |
} | |
call.enqueue(new Callback<List<GithubJob>>() { | |
@Override | |
public void onResponse(Call<List<GithubJob>> call, Response<List<GithubJob>> response) { | |
if (response.isSuccessful()) { | |
networkState.postValue(NetworkState.LOADED); | |
callback.onResult(response.body(), Integer.toString( page.get() + 1)); | |
if (response.body().size() > 0) { | |
for (GithubJob item : response.body()) { | |
githubJobObserve.onNext(item); | |
} | |
} | |
} else { | |
networkState.postValue(new NetworkState(NetworkState.Status.FAILED, response.message())); | |
} | |
} | |
@Override | |
public void onFailure(Call<List<GithubJob>> call, Throwable t) { | |
networkState.postValue(new NetworkState(NetworkState.Status.FAILED, Utils.errorMessageHandler(call, t))); | |
callback.onResult(new ArrayList<>(), Integer.toString(page.get())); | |
} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<layout xmlns:app="http://schemas.android.com/apk/res-auto"> | |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="10dp" | |
android:layout_marginTop="8dp" | |
android:layout_marginEnd="10dp" | |
android:orientation="vertical"> | |
<com.facebook.shimmer.ShimmerFrameLayout | |
android:id="@+id/shimmer" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical" | |
app:layout_behavior="@string/appbar_scrolling_view_behavior" | |
app:shimmer_duration="800" | |
app:shimmer_auto_start="true"> | |
<RelativeLayout | |
android:id="@+id/progress_bar" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginBottom="10dp" | |
android:layout_marginTop="10dp" > | |
<View | |
android:id="@+id/icon" | |
android:layout_width="60dp" | |
android:layout_height="60dp" | |
android:gravity="center" | |
android:layout_gravity="center" | |
android:layout_marginStart="10dp" | |
android:background="@drawable/bg_photo_item" /> | |
<View | |
android:id="@+id/title" | |
android:layout_marginStart="16dp" | |
android:layout_toEndOf="@id/icon" | |
android:layout_width="180dp" | |
android:layout_height="17dp" | |
android:layout_marginTop="2dp" | |
android:layout_marginEnd="16dp" | |
android:background="@drawable/bg_strip_round" /> | |
<View | |
android:id="@+id/subtitle" | |
android:layout_below="@id/title" | |
android:layout_marginStart="16dp" | |
android:layout_marginEnd="130dp" | |
android:layout_width="110dp" | |
android:layout_toEndOf="@id/icon" | |
android:layout_height="10dp" | |
android:layout_marginTop="10dp" | |
android:background="@drawable/bg_strip_round" /> | |
<View | |
android:layout_below="@id/subtitle" | |
android:layout_marginStart="16dp" | |
android:layout_marginEnd="130dp" | |
android:layout_marginBottom="20dp" | |
android:layout_width="140dp" | |
android:layout_toEndOf="@id/icon" | |
android:layout_height="10dp" | |
android:layout_marginTop="5dp" | |
android:background="@drawable/bg_strip_round" /> | |
</RelativeLayout> | |
</com.facebook.shimmer.ShimmerFrameLayout> | |
<TextView | |
android:visibility="gone" | |
android:id="@+id/error_msg" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_marginLeft="16dp" | |
android:layout_marginRight="16dp" | |
android:layout_marginTop="20dp" | |
android:layout_marginBottom="20dp" | |
android:layout_gravity="center_horizontal" /> | |
</LinearLayout> | |
</layout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Intent; | |
import android.os.Bundle; | |
import android.view.MenuItem; | |
import android.view.View; | |
import android.widget.Toast; | |
import androidx.annotation.NonNull; | |
import androidx.annotation.Nullable; | |
import androidx.lifecycle.ViewModelProvider; | |
import com.google.android.material.snackbar.Snackbar; | |
import javax.inject.Inject; | |
import id.haerulmuttaqin.jobfinder.R; | |
import id.haerulmuttaqin.jobfinder.base.BaseActivity; | |
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository; | |
import id.haerulmuttaqin.jobfinder.databinding.ActivityListBinding; | |
import id.haerulmuttaqin.jobfinder.ui.MainAdapter; | |
import id.haerulmuttaqin.jobfinder.ui.MainViewModel; | |
import id.haerulmuttaqin.jobfinder.ui.detail.DetailActivity; | |
public class ListActivity extends BaseActivity<ActivityListBinding, MainViewModel> implements MainViewModel.Navigator { | |
@Inject | |
GithubJobRepository repository; | |
@Inject | |
ConnectionServer server; | |
private ActivityListBinding binding; | |
private MainViewModel viewModel; | |
@Override | |
public int getBindingVariable() { | |
return 0; | |
} | |
@Override | |
public int getLayoutId() { | |
return R.layout.activity_list; | |
} | |
@Override | |
public MainViewModel getViewModel() { | |
return viewModel; | |
} | |
@Override | |
protected void onCreate(@Nullable Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
binding = getViewDataBinding(); | |
viewModel = new ViewModelProvider( | |
this, | |
new MainViewModel.ModelFactory(this, server, repository) | |
).get(MainViewModel.class); | |
viewModel.setNavigator(this); | |
if (getIntent().getStringExtra("recommended") != null) { | |
setupActionBar("Recommended Jobs"); | |
getData(null); | |
binding.swipeRefresh.setOnRefreshListener(()->getData(null)); | |
} | |
else if (getIntent().getStringExtra("search") != null) { | |
String keyword = getIntent().getStringExtra("search"); | |
setupActionBar("Search Jobs (" + keyword + ")"); | |
getData(keyword); | |
binding.swipeRefresh.setOnRefreshListener(()->getData(keyword)); | |
} | |
else if (getIntent().getStringExtra("marked") != null) { | |
setupActionBar("Marked Jobs"); | |
binding.swipeRefresh.setOnRefreshListener(null); | |
viewModel.getLiveDataMarked().observe(this, githubJobs -> { | |
hideProgress(); | |
if (githubJobs.size() > 0) { | |
binding.recyclerView.setAdapter(new MainAdapter(githubJobs, viewModel)); | |
} else { | |
binding.emptyView.setVisibility(View.VISIBLE); | |
binding.textEmptyErr.setText("No job marked!"); | |
} | |
}); | |
} | |
else { | |
Toast.makeText(this, "Failed get list!", Toast.LENGTH_SHORT).show(); | |
finish(); | |
} | |
} | |
private void getData(String keyword) { | |
final ListPagedAdapter adapter = new ListPagedAdapter(viewModel); | |
viewModel.getPagedData(keyword).observe(this, githubJobs -> { | |
adapter.submitList(githubJobs); | |
}); | |
viewModel.getNetworkState().observe(this, networkState -> { | |
adapter.setNetworkState(networkState); | |
binding.setState(networkState); | |
}); | |
binding.recyclerView.setAdapter(adapter); | |
} | |
private void setupActionBar(String title) { | |
setSupportActionBar(binding.toolbar); | |
if (getSupportActionBar() != null) { | |
getSupportActionBar().setDisplayHomeAsUpEnabled(true); | |
getSupportActionBar().setTitle(title); | |
} | |
} | |
@Override | |
public boolean onOptionsItemSelected(@NonNull MenuItem item) { | |
if (item.getItemId() == android.R.id.home) { | |
onBackPressed(); | |
return true; | |
} | |
return super.onOptionsItemSelected(item); | |
} | |
@Override | |
public void showProgress() { | |
binding.swipeRefresh.setRefreshing(true); | |
binding.emptyView.setVisibility(View.GONE); | |
binding.shimmer.setVisibility(View.VISIBLE); | |
binding.shimmer.startShimmer(); | |
binding.recyclerView.setVisibility(View.GONE); | |
} | |
@Override | |
public void hideProgress() { | |
binding.swipeRefresh.setRefreshing(false); | |
binding.emptyView.setVisibility(View.GONE); | |
binding.shimmer.setVisibility(View.GONE); | |
binding.shimmer.stopShimmer(); | |
binding.recyclerView.setVisibility(View.VISIBLE); | |
} | |
@Override | |
public void onGetResult(boolean status, String message) { | |
if (!status) { //<-- status result is FALSE | |
binding.textEmptyErr.setText(message); | |
binding.emptyView.setVisibility(View.VISIBLE); | |
} else { | |
binding.emptyView.setVisibility(View.GONE); | |
} | |
} | |
@Override | |
public void onMark(int mark, String title) { | |
Snackbar.make(binding.getRoot(), mark == 0 ? "\uD83D\uDE13 Unmark " + title : "\uD83D\uDE0D Marked " + title, Snackbar.LENGTH_SHORT).show(); | |
} | |
@Override | |
public void onItemClick(GithubJob githubJob) { | |
Intent intent = new Intent(this, DetailActivity.class); | |
intent.putExtra("item", githubJob); | |
startActivity(intent); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.SuppressLint; | |
import android.graphics.drawable.Drawable; | |
import android.view.LayoutInflater; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import androidx.annotation.NonNull; | |
import androidx.annotation.Nullable; | |
import androidx.databinding.DataBindingUtil; | |
import androidx.paging.PagedListAdapter; | |
import androidx.recyclerview.widget.RecyclerView; | |
import com.bumptech.glide.Glide; | |
import com.bumptech.glide.load.DataSource; | |
import com.bumptech.glide.load.engine.GlideException; | |
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; | |
import com.bumptech.glide.request.RequestListener; | |
import com.bumptech.glide.request.target.Target; | |
import id.haerulmuttaqin.jobfinder.R; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.entity.NetworkState; | |
import id.haerulmuttaqin.jobfinder.databinding.ItemJobBinding; | |
import id.haerulmuttaqin.jobfinder.databinding.ItemJobLoadingBinding; | |
import id.haerulmuttaqin.jobfinder.ui.MainViewModel; | |
public class ListPagedAdapter extends PagedListAdapter<GithubJob, RecyclerView.ViewHolder> { | |
private MainViewModel viewModel; | |
private NetworkState networkState; | |
public ListPagedAdapter(MainViewModel viewModel) { | |
super(GithubJob.DIFF_CALLBACK); | |
this.viewModel = viewModel; | |
} | |
@NonNull | |
@Override | |
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | |
if (viewType == R.layout.item_job) { | |
LayoutInflater inflater = LayoutInflater.from(parent.getContext()); | |
ItemJobBinding binding = DataBindingUtil.inflate(inflater, R.layout.item_job, parent, false); | |
return new RecyclerViewAdapter(binding); | |
} | |
else if (viewType == R.layout.item_job_loading) { | |
LayoutInflater inflater = LayoutInflater.from(parent.getContext()); | |
ItemJobLoadingBinding binding = DataBindingUtil.inflate(inflater, R.layout.item_job_loading, parent, false); | |
return new LoadingViewAdapter(binding); | |
} | |
else { | |
throw new IllegalArgumentException("unknown view type"); | |
} | |
} | |
@Override | |
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { | |
switch (getItemViewType(position)) { | |
case R.layout.item_job: | |
GithubJob item = getItem(position); | |
if (item != null) { | |
((RecyclerViewAdapter) holder).bind(item, viewModel); | |
} | |
break; | |
case R.layout.item_job_loading: | |
((LoadingViewAdapter) holder).bind(networkState); | |
break; | |
} | |
} | |
private boolean hasExtraRow() { | |
return networkState != null && networkState != NetworkState.LOADED; | |
} | |
@Override | |
public int getItemViewType(int position) { | |
if (hasExtraRow() && position == getItemCount() - 1) { | |
return R.layout.item_job_loading; | |
} else { | |
return R.layout.item_job; | |
} | |
} | |
public void setNetworkState(NetworkState networkState) { | |
this.networkState = networkState; | |
boolean previousExtraRow = hasExtraRow(); | |
NetworkState previousState = this.networkState; | |
boolean newExtraRow = hasExtraRow(); | |
if (previousExtraRow != newExtraRow) { | |
if (previousExtraRow) { | |
notifyItemRemoved(getItemCount()); | |
} | |
else { | |
notifyItemInserted(getItemCount()); | |
} | |
} | |
else if (newExtraRow && previousState != networkState) { | |
notifyItemChanged(getItemCount() - 1); | |
} | |
} | |
public static class RecyclerViewAdapter extends RecyclerView.ViewHolder { | |
ItemJobBinding binding; | |
public RecyclerViewAdapter(ItemJobBinding binding) { | |
super(binding.getRoot()); | |
this.binding = binding; | |
} | |
@SuppressLint("UseCompatLoadingForDrawables") | |
void bind(@NonNull GithubJob data, MainViewModel viewModel) { | |
Glide.with(binding.photoPreview.getContext()) | |
.load(data.companyLogo) | |
.error(binding.photoPreview.getContext().getDrawable(R.drawable.ic_round_business_center_24)) | |
.transition(DrawableTransitionOptions.withCrossFade()) | |
.listener(new RequestListener<Drawable>() { | |
@Override | |
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { | |
binding.progress.stopShimmer(); | |
binding.progress.setVisibility(View.GONE); | |
return false; | |
} | |
@Override | |
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { | |
binding.progress.stopShimmer(); | |
binding.progress.setVisibility(View.GONE); | |
return false; | |
} | |
}) | |
.into(binding.photoPreview); | |
binding.setItem(data); | |
binding.setViewModel(viewModel); | |
binding.executePendingBindings(); | |
} | |
} | |
public static class LoadingViewAdapter extends RecyclerView.ViewHolder { | |
ItemJobLoadingBinding binding; | |
public LoadingViewAdapter(ItemJobLoadingBinding binding) { | |
super(binding.getRoot()); | |
this.binding = binding; | |
} | |
@SuppressLint("UseCompatLoadingForDrawables") | |
void bind(NetworkState networkState) { | |
if (networkState != null && networkState.getStatus() == NetworkState.Status.RUNNING) { | |
binding.progressBar.setVisibility(View.VISIBLE); | |
} else { | |
binding.progressBar.setVisibility(View.GONE); | |
} | |
if (networkState != null && networkState.getStatus() == NetworkState.Status.FAILED) { | |
binding.errorMsg.setVisibility(View.VISIBLE); | |
binding.errorMsg.setText(networkState.getMsg()); | |
} else { | |
binding.errorMsg.setVisibility(View.GONE); | |
} | |
binding.executePendingBindings(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.annotation.NonNull; | |
import androidx.paging.DataSource; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobDao; | |
public class LocalDataSourceFactory extends DataSource.Factory { | |
private LocalPageKeyedDataSource localPageKeyedDataSource; | |
public LocalDataSourceFactory(GithubJobDao dao, String keyword) { | |
localPageKeyedDataSource = new LocalPageKeyedDataSource(dao, keyword); | |
} | |
@NonNull | |
@Override | |
public DataSource create() { | |
return localPageKeyedDataSource; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.annotation.NonNull; | |
import androidx.paging.PageKeyedDataSource; | |
import java.util.List; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobDao; | |
public class LocalPageKeyedDataSource extends PageKeyedDataSource<String, GithubJob> { | |
private final GithubJobDao dao; | |
private String keyword; | |
public LocalPageKeyedDataSource(GithubJobDao dao, String keyword) { | |
this.dao = dao; | |
this.keyword = keyword; | |
} | |
@Override | |
public void loadInitial(@NonNull LoadInitialParams<String> params, @NonNull LoadInitialCallback<String, GithubJob> callback) { | |
List<GithubJob> data = null; | |
if (keyword == null) { | |
data = dao.getList(); | |
} else { | |
data = dao.searchList(keyword); | |
} | |
if (data.size() != 0) { | |
callback.onResult(data, Integer.toString(0), Integer.toString(1)); | |
} | |
} | |
@Override | |
public void loadBefore(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, GithubJob> callback) { | |
} | |
@Override | |
public void loadAfter(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, GithubJob> callback) { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Intent; | |
import android.os.Bundle; | |
import android.view.View; | |
import android.widget.RelativeLayout; | |
import androidx.annotation.Nullable; | |
import androidx.lifecycle.ViewModelProvider; | |
import com.google.android.material.bottomsheet.BottomSheetDialog; | |
import com.google.android.material.snackbar.Snackbar; | |
import com.google.android.material.textfield.TextInputEditText; | |
import javax.inject.Inject; | |
import id.haerulmuttaqin.jobfinder.R; | |
import id.haerulmuttaqin.jobfinder.base.BaseActivity; | |
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository; | |
import id.haerulmuttaqin.jobfinder.databinding.ActivityMainBinding; | |
import id.haerulmuttaqin.jobfinder.ui.detail.DetailActivity; | |
import id.haerulmuttaqin.jobfinder.ui.list.ListActivity; | |
public class MainActivity extends BaseActivity<ActivityMainBinding, MainViewModel> implements MainViewModel.Navigator { | |
@Inject ConnectionServer server; | |
@Inject GithubJobRepository repository; | |
private ActivityMainBinding binding; | |
private MainViewModel viewModel; | |
@Override | |
public int getBindingVariable() { | |
return 0; | |
} | |
@Override | |
public int getLayoutId() { | |
return R.layout.activity_main; | |
} | |
@Override | |
public MainViewModel getViewModel() { | |
return viewModel; | |
} | |
@Override | |
protected void onCreate(@Nullable Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
binding = getViewDataBinding(); | |
viewModel = new ViewModelProvider(this, new MainViewModel.ModelFactory(this, server, repository)).get(MainViewModel.class); | |
viewModel.setNavigator(this); | |
viewModel.getJobFromServer(); | |
viewModel.getLiveData().observe(this, githubJobs -> { | |
binding.recyclerView.setAdapter(new MainAdapter(githubJobs, viewModel)); | |
}); | |
viewModel.getLiveDataMarked().observe(this, githubJobs -> { | |
if (githubJobs.size() > 0) { | |
binding.markedTitle.setVisibility(View.VISIBLE); | |
binding.recyclerViewMarked.setVisibility(View.VISIBLE); | |
binding.recyclerViewMarked.setAdapter(new MainMarkedAdapter(githubJobs, viewModel)); | |
} else { | |
binding.markedTitle.setVisibility(View.GONE); | |
binding.recyclerViewMarked.setVisibility(View.GONE); | |
} | |
}); | |
binding.swipeRefresh.setOnRefreshListener(()->viewModel.getJobFromServer()); | |
binding.cardSearch.setOnClickListener(v->{ | |
showDialogSearch(); | |
}); | |
binding.showAllMarked.setOnClickListener(v-> { | |
Intent intent = new Intent(this, ListActivity.class); | |
intent.putExtra("marked", "marked"); | |
startActivity(intent); | |
}); | |
binding.showAllRecommended.setOnClickListener(v-> { | |
Intent intent = new Intent(this, ListActivity.class); | |
intent.putExtra("recommended", "recommended"); | |
startActivity(intent); | |
}); | |
} | |
private void showDialogSearch() { | |
View view = getLayoutInflater().inflate(R.layout.dialog_search, null); | |
TextInputEditText m = view.findViewById(R.id.message); | |
RelativeLayout btnSearch = view.findViewById(R.id.search); | |
BottomSheetDialog dialog = new BottomSheetDialog(this, R.style.BottomSheetDialogStyle); | |
btnSearch.setOnClickListener(v -> { | |
Intent intent = new Intent(this, ListActivity.class); | |
intent.putExtra("search", m.getText().toString()); | |
startActivity(intent); | |
}); | |
m.requestFocus(); | |
dialog.setContentView(view); | |
dialog.show(); | |
} | |
@Override | |
public void showProgress() { | |
binding.swipeRefresh.setRefreshing(true); | |
binding.emptyView.setVisibility(View.GONE); | |
binding.shimmer.setVisibility(View.VISIBLE); | |
binding.shimmer.startShimmer(); | |
binding.contentLayout.setVisibility(View.GONE); | |
} | |
@Override | |
public void hideProgress() { | |
binding.swipeRefresh.setRefreshing(false); | |
binding.emptyView.setVisibility(View.GONE); | |
binding.shimmer.setVisibility(View.GONE); | |
binding.shimmer.stopShimmer(); | |
binding.contentLayout.setVisibility(View.VISIBLE); | |
} | |
@Override | |
public void onGetResult(boolean status, String message) { | |
if (!status) { //<-- status result is FALSE | |
binding.textEmptyErr.setText(message); | |
binding.emptyView.setVisibility(View.VISIBLE); | |
} else { | |
binding.emptyView.setVisibility(View.GONE); | |
} | |
} | |
@Override | |
public void onMark(int mark, String title) { | |
Snackbar.make(binding.getRoot(), mark == 0 ? "\uD83D\uDE13 Unmark " + title : "\uD83D\uDE0D Marked " + title, Snackbar.LENGTH_SHORT).show(); | |
} | |
@Override | |
public void onItemClick(GithubJob githubJob) { | |
Intent intent = new Intent(this, DetailActivity.class); | |
intent.putExtra("item", githubJob); | |
startActivity(intent); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context; | |
import androidx.annotation.NonNull; | |
import androidx.lifecycle.LiveData; | |
import androidx.lifecycle.ViewModel; | |
import androidx.lifecycle.ViewModelProvider; | |
import androidx.paging.PagedList; | |
import java.util.List; | |
import id.haerulmuttaqin.jobfinder.Utils; | |
import id.haerulmuttaqin.jobfinder.base.BaseViewModel; | |
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer; | |
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob; | |
import id.haerulmuttaqin.jobfinder.data.entity.NetworkState; | |
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository; | |
import retrofit2.Call; | |
import retrofit2.Callback; | |
import retrofit2.Response; | |
public class MainViewModel extends BaseViewModel<MainViewModel.Navigator> { | |
public MainViewModel(Context context, ConnectionServer connectionServer, GithubJobRepository repository) { | |
super(context, connectionServer, repository); | |
} | |
public LiveData<PagedList<GithubJob>> getPagedData(String keyword) { | |
return getRepository().getDataByPage(getConnectionServer(), getRepository(), keyword); | |
} | |
public LiveData<NetworkState> getNetworkState() { | |
return getRepository().getNetworkState(); | |
} | |
public LiveData<List<GithubJob>> getLiveData() { | |
return getRepository().getLiveData(); | |
} | |
public LiveData<List<GithubJob>> searchLiveData(String keyword) { | |
return getRepository().searchLiveData(keyword.toLowerCase()); | |
} | |
public LiveData<List<GithubJob>> getLiveDataMarked() { | |
return getRepository().getLiveDataMarked(); | |
} | |
public String formatDate(String date){ | |
return Utils.dateToTimeFormat(date); | |
} | |
public void getJobFromServer() { | |
getNavigator().showProgress(); | |
getConnectionServer().getJobList().enqueue(new Callback<List<GithubJob>>() { | |
@Override | |
public void onResponse(Call<List<GithubJob>> call, Response<List<GithubJob>> response) { | |
if (response.isSuccessful() && response.body().size() > 0) { | |
for (GithubJob item : response.body()) { | |
item.createdAt = Utils.dateFormatter(item.createdAt); // format date | |
getRepository().insert(item); | |
} | |
getNavigator().onGetResult(true, "Success"); | |
} | |
getNavigator().hideProgress(); | |
} | |
@Override | |
public void onFailure(Call<List<GithubJob>> call, Throwable t) { | |
t.getLocalizedMessage(); | |
getNavigator().hideProgress(); | |
getNavigator().onGetResult(false, Utils.errorMessageHandler(call, t)); | |
} | |
}); | |
} | |
public void searchJobFromServer(String keyword) { | |
getNavigator().showProgress(); | |
getConnectionServer().searchJobList(keyword).enqueue(new Callback<List<GithubJob>>() { | |
@Override | |
public void onResponse(Call<List<GithubJob>> call, Response<List<GithubJob>> response) { | |
if (response.isSuccessful() && response.body().size() > 0) { | |
for (GithubJob item : response.body()) { | |
getRepository().insert(item); | |
} | |
getNavigator().onGetResult(true, "Success"); | |
} | |
getNavigator().hideProgress(); | |
} | |
@Override | |
public void onFailure(Call<List<GithubJob>> call, Throwable t) { | |
t.getLocalizedMessage(); | |
getNavigator().hideProgress(); | |
getNavigator().onGetResult(false, Utils.errorMessageHandler(call, t)); | |
} | |
}); | |
} | |
public void markJob(GithubJob githubJob) { | |
getRepository().updateMarkJob(githubJob); | |
getNavigator().onMark(githubJob.is_mark, githubJob.title); | |
} | |
public void itemClick(GithubJob githubJob) { | |
getNavigator().onItemClick(githubJob); | |
} | |
public static class ModelFactory implements ViewModelProvider.Factory { | |
private Context context; | |
private ConnectionServer server; | |
private GithubJobRepository repository; | |
public ModelFactory(Context context, ConnectionServer server, GithubJobRepository repository) { | |
this.context = context; | |
this.server = server; | |
this.repository = repository; | |
} | |
@NonNull | |
@Override | |
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { | |
return (T) new MainViewModel(context, server, repository); | |
} | |
} | |
public interface Navigator { | |
void showProgress(); | |
void hideProgress(); | |
void onGetResult(boolean status, String message); | |
void onMark(int mark, String title); | |
void onItemClick(GithubJob githubJob); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class NetworkState { | |
public NetworkState(Status status, String msg) { | |
this.status = status; | |
this.msg = msg; | |
} | |
public enum Status { | |
FIRST_LOADING, | |
FIRST_FAILED, | |
RUNNING, | |
SUCCESS, | |
EMPTY, | |
FAILED | |
} | |
private final Status status; | |
private final String msg; | |
public static final NetworkState LOADED; | |
public static final NetworkState LOADING; | |
public static final NetworkState FIRST_LOADING; | |
public static final NetworkState FIRST_FAILED; | |
public static final NetworkState EMPTY; | |
static { | |
LOADED = new NetworkState(Status.SUCCESS,"Success"); | |
LOADING = new NetworkState(Status.RUNNING,"Running"); | |
FIRST_LOADING = new NetworkState(Status.FIRST_LOADING,"First Runnig"); | |
FIRST_FAILED = new NetworkState(Status.FIRST_FAILED,"First Failed"); | |
EMPTY = new NetworkState(Status.EMPTY, "Empty"); | |
} | |
public Status getStatus() { | |
return status; | |
} | |
public String getMsg() { | |
return msg; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment