Android provides the PdfRenderer class for rendering PDF files within an app. While it allows for PDF rendering, the implementation can be quite tedious process, as it requires managing tasks like opening the document, rendering individual pages, and handling scaling etc. For a quicker and more efficient solution, using a reliable third-party library is often a better option.
Today, in this article we'll see how to render PDF files using AndroidPdfViewer library. We'll cover how to load the PDF from local memory and from a network URL using the Retrofit library.
First add the PDFView component to the layout file.
And load the PDF using any of the methods below. You need to implement the file chooser intent to get the file Uri.
I hope this article provided good insights into rendering PDF files on Android. You can refer the full code here.
Cheers!
Happy Coding 🤗
Today, in this article we'll see how to render PDF files using AndroidPdfViewer library. We'll cover how to load the PDF from local memory and from a network URL using the Retrofit library.
1. Adding the dependencies
Let's start by creating a new project in Android Studio and add the necessary dependencies.- In Android Studio, create a new project from File => New Project and choose Empty View Activity to create the project using Kotlin language.
-
Open the app's build.gradle and add the
PDF
library depency. We would also need to add
Retrofit
and
Okhttp
libraries to make the network call.
build.gradle
dependencies { // LiveData & ViewModels implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6" // PDF viewer implementation "com.github.barteksc:android-pdf-viewer:3.2.0-beta.1" // Retrofit implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14" implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation "com.squareup.retrofit2:retrofit:2.11.0" implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"}
2. Loading local PDF
The PDF library provides PDFView component that can be included in your xml layout. Loading the PDF from local storage is pretty straight forward.First add the PDFView component to the layout file.
<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
// provide the document to load the PDF
pdfView.fromUri(Uri)
// or use the other methods
pdfView.fromFile(File)
pdfView.fromBytes(byte[])
pdfView.fromStream(InputStream)
pdfView.fromSource(DocumentSource)
3. Loading PDF from Remote URL
The library doesn't offer a method to render the PDF from a remote URL directly. Instead the file has to be downloaded first and then rendered. To download the file, we are going to use Retrofit networking library. Below is the final project structure that we are going to create shortly.3.1 Adding Retrofit Library
In this tutorial, we are going to keep very basic setup needed for Retrofit. You can further customise the library and use a dependency framework like Hilt for easier integration.{alertInfo}
-
Create a new package called utils and create a class named
SingletonHolder.kt. Using this class, we can create the singleton
instance of class objects.
SingletonHolder.kt
package info.androidhive.androidpdf.util open class SingletonHolder<out T: Any, in A>(creator: (A) -> T) { private var creator: ((A) -> T)? = creator @Volatile private var instance: T? = null fun getInstance(arg: A): T { val i = instance if (i != null) { return i } return synchronized(this) { val i2 = instance if (i2 != null) { i2 } else { val created = creator!!(arg) instance = created creator = null created } } } }
- Create another package called remote and create NullOnEmptyConverterFactory.kt, Api.kt and ApiService.kt files under it.
NullOnEmptyConverterFactory.kt
package info.androidhive.androidpdf.remote import okhttp3.ResponseBody import retrofit2.Converter import retrofit2.Retrofit import java.lang.reflect.Type internal class NullOnEmptyConverterFactory private constructor() : Converter.Factory() { override fun responseBodyConverter( type: Type, annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *> { val delegate: Converter<ResponseBody, *> = retrofit.nextResponseBodyConverter<Any>(this, type, annotations) return Converter { body -> if (body.contentLength() == 0L) { "{}" // Empty JSON element } else delegate.convert(body) } } companion object { fun create(): Converter.Factory { return NullOnEmptyConverterFactory() } } }
Api.ktpackage info.androidhive.androidpdf.remote import android.content.Context import com.google.gson.GsonBuilder import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory import info.androidhive.androidpdf.util.SingletonHolder import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class Api(private val context: Context) { val apiService: ApiService by lazy { retrofit().create(ApiService::class.java) } private fun retrofit(): Retrofit { return Retrofit.Builder().addCallAdapterFactory(CoroutineCallAdapterFactory()) .client(okhttpClient()).addConverterFactory(NullOnEmptyConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) .addCallAdapterFactory(CoroutineCallAdapterFactory()).baseUrl("https://mydomain.com") .build() } private fun okhttpClient(): OkHttpClient { val interceptor = HttpLoggingInterceptor() interceptor.setLevel(HttpLoggingInterceptor.Level.BODY) return OkHttpClient.Builder() .addInterceptor(Interceptor { chain: Interceptor.Chain -> val request: Request = chain.request().newBuilder() .build() chain.proceed(request) }).build() } companion object : SingletonHolder<Api, Context>(::Api) }
ApiService.ktpackage info.androidhive.androidpdf.remote import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Url interface ApiService { @GET fun getFile(@Url url: String?): Call<ResponseBody> }
3.2 Rendering PDF from URL
Now that we have the network layer ready, let's create an activity to display the PDF. This activity follows the basic MVVM structure that involves creating an activity, fragment, view model and a repository class.- Create a new package called pdf to add keep all the pdf releated files under one package
- Right click on pdf package and select New => Activity => Fragment + ViewModel and name them as ViewPdfActivity, ViewPdfFragment, and ViewPdfViewModel.
- Open AndroidManifest.xml and add INTERNET permission. Add configChanges attribute to ViewPdfActivity to avoid restarting the activity on device orientation changes.
SingletonHolder.kt
<uses-permission android:name="android.permission.INTERNET" /> <activity android:name=".ui.pdf.ViewPdfActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:exported="false" android:parentActivityName=".ui.home.MainActivity" />
- Create ViewPdfRepository.kt and add the below content. This repository takes care of making the HTTP call and fetches the pdf file content.
ViewPdfRepository.kt
package info.androidhive.androidpdf.ui.pdf import info.androidhive.androidpdf.remote.ApiService import okhttp3.ResponseBody import retrofit2.Response class ViewPdfRepository(private val api: ApiService) { fun getFile(url: String?): Response<ResponseBody>? { return try { api.getFile(url).execute() } catch (e: Exception) { null } } }
- Create ViewPdfViewModel.kt and add the below code. This viewmodel talks to repository and gets the file data into a live data variable. This live data will be observed in the fragment.
ViewPdfViewModel.kt
package info.androidhive.androidpdf.ui.pdf import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import info.androidhive.androidpdf.remote.Api import info.androidhive.androidpdf.remote.ApiService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.ResponseBody import retrofit2.Response class ViewPdfViewModel(application: Application) : AndroidViewModel(application) { private val apiService: ApiService = Api.getInstance(application).apiService private val repository: ViewPdfRepository = ViewPdfRepository(apiService) private val _fileStream: MutableLiveData<Response<ResponseBody>> = MutableLiveData() val fileStream: LiveData<Response<ResponseBody>?> get() = _fileStream fun getFile(url: String?) { viewModelScope.launch(Dispatchers.IO) { _fileStream.postValue(repository.getFile(url)) } } }
- Open the layout file of the fragment fragment_view_pdf.xml and add the PDFView component. We are also adding a progress indicator that will be displayed while the file is being downloaded.
fragment_view_pdf.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".ui.pdf.ViewPdfFragment"> <com.github.barteksc.pdfviewer.PDFView android:id="@+id/pdf_viewer" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.google.android.material.progressindicator.LinearProgressIndicator android:id="@+id/progress_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="50dp" android:indeterminate="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
- Finally open ViewPdfFragment.kt and do the below changes. Here we pass the PDF url to view model and display the PDF once it is downloaded.
ViewPdfFragment.kt
package info.androidhive.androidpdf.ui.pdf import androidx.fragment.app.viewModels import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import info.androidhive.androidpdf.R import info.androidhive.androidpdf.databinding.FragmentViewPdfBinding class ViewPdfFragment : Fragment() { private val binding by lazy { FragmentViewPdfBinding.inflate(layoutInflater) } private val viewModel: ViewPdfViewModel by viewModels() private var title: String? = null private var pdfUrl: String? = null companion object { fun newInstance(bundle: Bundle?) = ViewPdfFragment().apply { arguments = bundle } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { title = it.getString("title") pdfUrl = it.getString("pdf_url") } } private fun bindObservers() { viewModel.fileStream.observe(this) { response -> if (response?.isSuccessful == true) { binding.apply { binding.pdfViewer.fromStream(response.body()?.byteStream()) .onLoad { progressBar.hide() } .onError { progressBar.hide() } .load() } } else { binding.progressBar.hide() Toast.makeText(activity, R.string.error_preview_pdf_file, Toast.LENGTH_SHORT).show() } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) bindObservers() // toolbar title activity?.title = title // fetch pdf from remote url viewModel.getFile(pdfUrl) } }
- Now, to view the PDF, launch the pdf viewer activity by passing the PDF HTTP url.
SingletonHolder.kt
binding.btnSample1.setOnClickListener { openPdf( "Lorem ipsum ", // pdf title "https://firebasestorage.googleapis.com/v0/b/project-8525323942962534560.appspot.com/o/samples%2Fpdf%2Ffile-example_PDF_1MB.pdf?alt=media&token=ea88122f-0524-4022-b401-f8ec1035901f" ) } // function to open pdf viewer activity private fun openPdf(title: String, url: String) { startActivity(Intent(this, ViewPdfActivity::class.java).apply { putExtra("title", title) putExtra("pdf_url", url) }) }
I hope this article provided good insights into rendering PDF files on Android. You can refer the full code here.
Cheers!
Happy Coding 🤗