Realm Weekly Bytes #7: Mobile to MongoDB Atlas - BookLog Application

G’Day, Folks,

I am super excited to share the BookLog Android Application that I created in continuation of the Realm Relationships topic we did a few weeks ago. It’s a basic application to show how the Relationships get synced to Atlas :smiley:

I would absolutely love your participation and feedback on this application :smiley:

I am assuming you already know the starting steps. If you are not comfortable with any of them, please feel free to raise a question below.

Part 01: Setting up Realm Backend

  1. Create an account on realm.mongodb.com
  2. Create an organization and a project
  3. Choose cluster tier and Cloud Provider. I chose M0 cluster, AWS in Ireland location
  4. Create a Realm Application from the Realm tab. I selected build from scratch, Ireland location, and local deployment model
  5. Enable Authentication Provider (I enabled email/password)

Now, the next step is to enable sync. I talked about Sync Types in a previous Realm Byte.

As I am developing the app from scratch, I will turn on development mode and will use partition sync.

Before I move on with Sync, let’s talk a little bit on Application use-case

What is BookLog Application

I am fond of books, so the idea was borne out of that, as well as the Relationship topic we talked about previously :smiling_face: The application is maintaining a list of books and author names. There are three screens

1. Add Author

This is a simple screen with an input component for the author and a button to add.

2. Add Book

This screen adds a book and selects the authors from the available list. If the author is not available, you will need to add the Author from the Author Screen. It also has a boolean variable if the book has been read.

3. List of Books with Author Names

This displays the list of books and authors added in the BookLog

How do I decide the partition strategy

The Strategy will depend on how I want my application to be used:

  • If I want all users to see the list of books and authors added, and have the permissions to add them as well, I would need to have a public partition with both read and write permissions to True. This will be termed as Firehose strategy

  • If I want users to create their own book log and add authors and books, then that will be read and write to a user-specific partition. With this setting, one user cannot read the booklist of another user. This is termed User Strategy.

For first, there is no partition at all i.e all data in Atlas will get synced to the device. It’s a public partition. The second is the most commonly used user-based partition. For the BookLog app, I am using the second option.

Permissions defined: Users can read and write to their own partitions, depicted in the diagram below:

With Development Mode on, this is all that needs to be done on the Realm Backend.

A sneak peek to your schema section will show you an empty schema (if this is your first application) or it will show the databases of your existing Realm apps.

Part 02: Setting up Android Application

Now the next step is to create the BookLog Android application, add dependencies, link to Realm Cloud and Sync data from Mobile to Atlas

I have mentioned some starting steps, but please feel free to ask below if you have any questions.

For adding Realm dependencies in your mobile application, please refer to Realm SDK docs as applicable.

  1. Add Realm dependencies to Gradle file
  2. Create an Application subclass and add the Realm App Id from the Realm Backend in this class.
  3. Create a Login Activity and implement code for email/password as that was enabled in previous steps.

Now, the next step is to create our Book and Author model classes. This is the same definition as mentioned in the Realm Relationships bytes a few weeks ago.

For brevity purposes, I will be limiting the coding to Schema Models and Queries and exclude any UI binding from this.

Author

open class Author(

   @PrimaryKey
   var _id: ObjectId = ObjectId(),

   @Required
   var name: String = "",

   @LinkingObjects("authors")
   val books: RealmResults<BookRealm>? = null
): RealmObject() { }

Book

open class BookRealm(
   @PrimaryKey
   var _id: ObjectId = ObjectId(),

   @Required
   var name: String = "",

   var isRead: Boolean = false,

   var _partition: String ="",

   var authors: RealmList<Author> = RealmList()
) : RealmObject() {}

If I want this schema to sync to Atlas, there are some additional points to take care of

  • Every model will need _id as the Primary Key of any type (ObjectId(), String, Int). This is a mandatory requirement as the MongoDB model identifies a document’s uniqueness with this identifier. Keeping any other field as the primary key will throw an error.
  • Every schema will need a _partition (partition key field), with the same name as mentioned in Realm Cloud settings
  • Every model will be a subclass of the RealmObject class. Another way is to implement the RealmModel and use @RealmClass annotation. Please follow RealmModel for more information.

Queries to add Author object to database

//1
var bookLogApp: App = App(AppConfiguration.Builder(appId).build())
//2
val config = SyncConfiguration.Builder(bookLogApp.currentUser(), 
bookLogApp.currentUser()?.id)
   .waitForInitialRemoteData(500, TimeUnit.MILLISECONDS)
   .build()
//3
Realm.getInstanceAsync(config, object : Realm.Callback() {
   override fun onSuccess(realm: Realm) {
           realmClass = realm
        createAuthorObject(“J.K Rowling”)
   }
})
//4
private fun createAuthorObject(authorName: String) {

   realmClass.executeTransactionAsync({
       val author = it.createObject(Author::class.java, ObjectId())
       //configure the instance
       author.name = authorName
   }, {
       Log.d("Successfully Added")
   }, {error->
       Log.e("Error adding Author %s",error.localizedMessage)
   })
}

//5
realmClass.close()

The code explanation is as below:

  1. This creates the Realm App instance, appId here is the Realm App Id copied from the backend.

  2. Please note, for Synced Realms use the “SyncConfiguration” API to build sync configuration and method call takes in the current user logged in and the partition value as arguments.

  3. This will create a Realm instance asynchronously i.e in a background thread and call a method to add Author name. This logic can vary based on a use-case and this is only a simple representation of it.

  4. The Write operations should always happen in a transaction, and the transaction here is opened asynchronously so that it does not block the UI thread. The author object is created and the author is saved to the database.

  5. It is important to close the Realm instance, otherwise it leads to memory leaks and increase in the size of the realm on disk.

Queries to add Book object to database

//1
Create app instance

//2
Create Sync config

//3
Create realm instance (no call to author func)

//4
realmClass.executeTransactionAsync ({realm ->
      //Option 1
    val bookToAdd:Book = realm.createObject(Book::class.java, ObjectId())
       bookToAdd.name = “Harry Potter and Chamber of Secrets”
       bookToAdd.isRead = true
       bookToAdd.authors.add(“J.K Rowling”)
     
      //Option 2  
      realm.insertOrUpdate(bookObject)

      //Option 3
      realm.copyToRealmOrUpdate(bookObject)


      
}, {
   Log.d("Book Object Added Successfully")

}, {throwError ->
   Log.d("Error adding the bookObject to Database %s", throwError.localizedMessage)
   }
})

//5
realmClass.close()

The first three steps will be the same as in add Author section with the difference that no call will be made to the Author function

  1. There are different options available to add the book object to the database depending on the workflow and architecture of your application.

  2. Don’t forget to close the Realm instance.

Tomorrow I will show how I displayed the list of books and authors, some errors I faced while creating the application, and share the full application code with you :smiley:

Cheers, :performing_arts:

1 Like

Errors in Book Log Application

Making the BookLog app was not an easy ride, I will share some of the errors I had and I would love to know your experiences in your app making using partition sync.

Additive Only Schema Change

I found out that even with the development mode On, I cannot make destructive changes to the schema. I had a required partition field in the BookRealm class but when I made it optional, I get below error:

2022-03-09 22:39:50.931 6928-6928/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.geek.booklog, PID: 6928
    java.lang.IllegalStateException: The following changes cannot be made in additive-only schema mode:
    - Property 'BookRealm._partition' has been made optional.

The utility of the development mode is still limited and does not give developers the ability to make destructive changes. The destructive changes are made directly from the Cloud config, and you are required to terminate and re-enable sync after a destructive change.

BAD_CHANGESET(realm::sync::ProtocolError:212): Bad changeset (UPLOAD)

This error is happening because somehow my partition value is getting set to null and I need to figure out what part of the code is doing that.

UPDATE instruction had incorrect partition value for key "_partition" { expectedPartition: 6228cd8243ec7f3ccbc246c2, foundPartition:

Permission Denied: User does not have write access to the partition

You have to be very careful of this error. Although the data from mobile will get synced to Atlas, but the write check is done before saving the data to Atlas. There is a possibility that the app may not crash after this error and you may not come to know why the data did not sync unless you check your logcat or any attached debugging system.
I realised that for BookLog App, I had not given correct permissions that were leading to this error.

After the above queries, when the app is synced to Atlas, the schemas on Atlas looks like below:

Author

Book

As you noticed, the back-links are not synced to Atlas. The Relationship does exist but is not synced to Atlas. when you query it, you will be able to see.

In the next session, I will share my findings on how I solved the partition error and how to display the list of books and authors.

Cheers, :performing_arts:

1 Like

Complete BookLog Application

G’Day Folks,

I am excited to share that I solved the missing puzzle pieces of this application :wink: It was a fun learning ride indeed.

The Bad Changeset error that I got previously was fixed after I logged in with a different user :smiley: I am very keen to know if you came across this similar error in your application and we could bang heads together on that… :blush:

Queries to display the list of Books and Authors

I am using the Recycler View library to display the list of books and Realm provides a recycler adapter that can easily display Realm Lists without changing the type to Array List

The code to set up Recycler View and execute the query to fetch Books is :

//realmlist is a copy of realm instance created after defining the config

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Timber.d("onCreate")
        user = bookLogApp.currentUser()

        Timber.d("User is $user")
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(requireContext(), LoginActivity::class.java))
        } else {

            val config = SyncConfiguration.Builder(
                bookLogApp.currentUser(),
                bookLogApp.currentUser()?.id
            ).build()

            realmList = Realm.getInstance(config)
        }
    }

//layout for recycler view is set after the view is created
   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        listBinding?.rvBookList?.layoutManager = LinearLayoutManager(context)
        setUpRecyclerView()
    }

//recycler view is also setup after view gets created and realm recycler adapter is passed with realm list of books
    private fun setUpRecyclerView() {
       adapter = BookListAdapter(realmList.where(BookRealm::class.java).sort("name").findAll())
        listBinding?.rvBookList?.adapter = adapter
    }

The code for recycler adapter and view holder is as follows:

class BookListAdapter(books: OrderedRealmCollection<BookRealm>): RealmRecyclerViewAdapter<BookRealm, BookListHolder>(books, true) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookListHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.booklist_item_layout, parent, false)
        return BookListHolder(view)
    }

    override fun onBindViewHolder(holder: BookListHolder, position: Int) {
        val book = getItem(position)
        holder.bindValues(book)
    }
}

class BookListHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    private val bookName = itemView.bookName
    private val authorName = itemView.authorName
    val stringBuilder = StringBuilder("")

    fun bindValues(book: BookRealm?){
        bookName.text = book?.name
        book?.authors?.forEach{
            stringBuilder.append(it.name).append(" ")
        }
        authorName.text = stringBuilder
    }

The Booklist display looks like below:

Few things to note:

  • This is an MVP app showing how book and author model classes are created and relationships established
  • The app explains basic CRUD operations to perform to add book and author to the database and retrieve the list
  • The app requires you have the required author added before you add the book. The edge cases of author not available or empty inputs may not be handled in the app.
  • The inverse relationships are not synced to Atlas but they do exist.
  • The app initially had a public strategy but it’s edited to user strategy now. Each user can read and write books and authors to their own realms/partitions.

The link to the full application is available on Github. :smile: Please feel free to suggest any feedback that you may have.

Happy Realming, :performing_arts:

1 Like