1:- Add the relevant library

Add library for Retrofit

// Retrofit
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
// OkHttp
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2")
// JSON Converter
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")

Add library for Hilt

Plugin
id("com.google.dagger.hilt.android")
id("kotlin-kapt")

Dependencies

kapt ("com.google.dagger:hilt-compiler:2.48")
implementation ("androidx.hilt:hilt-navigation-compose:1.0.0-alpha03")// if you are using navigation
implementation ("com.google.dagger:hilt-android:2.48")

In top-level module

id ("com.google.dagger.hilt.android") version "2.48" apply false

For Navigation

implementation(libs.androidx.navigation.compose)
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
navigationCompose = "2.7.7"

For Coroutine

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2")

For Coroutine Lifecycle Scopes

implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.5")

For Material Icon

implementation ("androidx.compose.material:material:1.0.4")
implementation ("androidx.compose.material:material-icons-extended:1.0.4")

For image loading

implementation("io.coil-kt:coil-compose:1.4.0")

2:- Create a Base Class for hilt

@HiltAndroidApp
class BaseApp:Application() {
}

3:- Create an interface and annotate with @Singleton

@Singleton
interface ApiInterface {
    @GET("volumes")
    suspend fun getAllBooks(@Query("q") query: String): Book

    @GET("volumes/{bookId}")
    suspend fun getBookDetails(@Path("bookId") bookId: String): Item

}

3:- Create app module to provide retrofit instance, Reositiry, and API interface instance

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Singleton
    @Provides
    fun provideRetrofit():Retrofit = Retrofit.Builder()
        .baseUrl(Constants.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()


    @Provides
    @Singleton
    fun provideApiInterface(retrofit: Retrofit): ApiInterface = retrofit.create(ApiInterface::class.java)

    @Provides
    @Singleton
    fun provideDataRepository(apiService: ApiInterface): BookRepository {
        return RepoImplementation(apiService)
    }
}

4:- Create Repository Interface

 interface BookRepository {
 suspend fun getAllBook(searchQuery: String): Results<List<Item>>
 suspend fun getBookDetails(bookId:String):Results<Item>
}

5:-Create repository Implementation

class RepoImplementation @Inject constructor(private  val apiInterface: ApiInterface):BookRepository{
    override suspend fun getAllBook(searchQuery: String): Results<List<Item>> {
        return try {
            Results.Loading(data = true)
            val data = apiInterface.getAllBooks(searchQuery).items
            Results.Success(data = data)
        } catch (e:Exception)
        {
            Results.Error(msg = e.message.toString())

        }    }

    override suspend fun getBookDetails(bookId: String): Results<Item> {
        return  try {
            Results.Loading(data = true)
            val data = apiInterface.getBookDetails(bookId)
            Log.e("NetworkData", "data $data", )

            Results.Success(data = data)
        } catch (e:Exception)
        {
            Results.Error(msg = e.message.toString())
        }    }

}

6:-Create ViewModel 

@HiltViewModel
class BookListViewModel @Inject constructor(private val repository: BookRepository)
    : ViewModel() {
    var list: List<Item> by mutableStateOf(listOf())
    var isLoading: Boolean by mutableStateOf(true)
    init {
        loadBooks()
    }

    private fun loadBooks() {
        searchBooks("android")
    }

    fun searchBooks(query: String) {
        viewModelScope.launch(Dispatchers.Default) {

            if (query.isEmpty()){
                return@launch
            }
            val response = repository.getAllBook(query)
            Log.e("DATA", "${response.data}", )

            try {
                when(val response = repository.getAllBook(query)) {
                    is Results.Success -> {
                        list = response.data!!
                        if (list.isNotEmpty()) isLoading = false
                        Log.e("NetworkData", "${list[0].volumeInfo.imageLinks.smallThumbnail}", )

                    }
                    is Results.Error -> {
                        isLoading = false
                        Log.e("Network", "searchBooks: Failed getting books", )
                    }
                    else -> {isLoading = false}
                }

            }catch (exception: Exception){
                isLoading = false
                Log.d("Network", "searchBooks: ${exception.message.toString()}")
            }

        }


    }


}

7:- Create a class to store api state

sealed class Results<T>(val data:T? = null, val msg:String?=null) {
    class Success<T>(data: T): Results<T>(data)
    class  Error<T>(msg:String,data: T?=null):Results<T>(data,msg)
    class Loading<T>(data: T?= null):Results<T>(data)
}

8:- Now comes to the UI part 

1:- Create a Navigation graph to navigation

@Composable
fun ComposeNavGraph() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = BookScreens.ListScreen.name
    ) {
        // Define your ListScreen route
        composable(BookScreens.ListScreen.name) {
            BookListScreen(navController = navController)
        }

        // Define your DetailsScreen route
        val detailName = BookScreens.DetailsScreen.name
        composable(
            route = "$detailName/{bookId}",
            arguments = listOf(navArgument("bookId") {
                type = NavType.StringType
            })
        ) { backStackEntry ->
            val bookId = backStackEntry.arguments?.getString("bookId")
            val detailsViewModel = hiltViewModel<DetailsViewModel>()
            ReaderBookDetailsScreen(
                navController = navController,
                bookId = bookId.orEmpty(),
                viewModel = detailsViewModel
            )
        }
    }
}
enum class BookScreens {

    ListScreen,
    DetailsScreen;
    companion object{
        fun fromRoute(rounte:String):BookScreens
        = when (rounte?.substringBefore("/")){
            ListScreen.name ->ListScreen
            DetailsScreen.name ->DetailsScreen

            null ->ListScreen
            else ->throw IllegalArgumentException("Route $rounte is not recognized")
    }
}
}

2:- Create List screen

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun BookListScreen(
    navController: NavHostController,
    viewModel: BookListViewModel = hiltViewModel()
) {
    Scaffold(topBar = { SimpleTopBar("Book List") }) {
        Surface() {

            Column {
                SearchForm(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(6.dp),
                    viewModel = viewModel
                ) { searchQuery ->
                    viewModel.searchBooks(query = searchQuery)

                }
                Spacer(modifier = Modifier.height(13.dp))
                BookList(navController = navController)


            }

        }


    }
}
@Composable
fun BookList(navController: NavHostController, viewModel: BookListViewModel = hiltViewModel()) {
    var list = viewModel.list
    if (viewModel.isLoading) {
        CenteredLoadingIndicator()
    } else {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(16.dp)
        ) {
            items(items = list) { book ->
                BookRow(book, navController)

            }

        }
    }

}
@Composable
fun BookRow(book: Item, navController: NavHostController) {
    Card(
        modifier = Modifier
            .clickable {
                navController.navigate(BookScreens.DetailsScreen.name + "/${book.id}")
            }
            .fillMaxWidth()
            .height(100.dp)
            .padding(3.dp),
        shape = RectangleShape,
        elevation = 7.dp
    ) {
        Row(
            modifier = Modifier
                .padding(5.dp),
            verticalAlignment = Alignment.Top
        ) {

            val imageUrl: String = if (book.volumeInfo.imageLinks.smallThumbnail.isEmpty())
                "https://images.unsplash.com/photo-1541963463532-d68292c34b19?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=80&q=80"
            else {
                book.volumeInfo.imageLinks.smallThumbnail
            }

            Image(
                painter = rememberImagePainter(data = imageUrl),
                contentDescription = "book image",
                modifier = Modifier
                    .width(80.dp)
                    .fillMaxHeight()
                    .padding(end = 4.dp),
            )

            Column {
                Text(text = book.volumeInfo.title.toString(), overflow = TextOverflow.Ellipsis)
                Text(
                    text = "Author: ${book.volumeInfo.authors}",
                    overflow = TextOverflow.Clip,
                    fontStyle = FontStyle.Italic,
                    style = MaterialTheme.typography.caption
                )

                Text(
                    text = "${book.volumeInfo.categories}",
                    overflow = TextOverflow.Clip,
                    fontStyle = FontStyle.Italic,
                    style = MaterialTheme.typography.caption
                )

            }

        }


    }

}
@Composable
fun SearchForm(
    modifier: Modifier = Modifier,
    viewModel: BookListViewModel,
    loading: Boolean = false,
    hint: String = "Search",
    onSearch: (String) -> Unit = {}
) {
    Column {
        val searchQueryState = rememberSaveable { mutableStateOf("") }
        val keyboardController = LocalSoftwareKeyboardController.current
        val valid = remember(searchQueryState.value) {
            searchQueryState.value.trim().isNotEmpty()

        }

        InputField(valueState = searchQueryState,
            labelId = "Search",
            enabled = true,
            onAction = KeyboardActions {
                if (!valid) return@KeyboardActions
                onSearch(searchQueryState.value.trim())
                searchQueryState.value = ""
                keyboardController?.hide()
            })

    }
}

3:- Main Activity


@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TestTheme {
                screensNavigation()
            }
        }
    }
}

@Composable
fun screensNavigation(){
     Surface (
         color = MaterialTheme.colorScheme.background,
         modifier = Modifier.fillMaxSize(),
         content = {
             Column (verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally)
             {
                 ComposeNavGraph()
             }


         }

     )

}

You may also like...

0 Comments

No Comment.