Search code examples
androidfluttermigration

How to efficiently migrate an Android app to Flutter


I am looking for the best way to migrate an Android app to Flutter. Sure, I could simply develop everything again from scratch, but that seems way to inefficient.

Are there any best practices on how to efficiently migrate/redevelop an existing Android app to Flutter?

More precisely, I am looking for something concerning this:

  • Although I doubt its existence, I would love to simply give a tool my full Android project directory and have it convert any files it can convert into something Flutter/Dart-like.
  • How to convert models and simple structure files efficiently into Dart code?
public class CheckboxListViewModel {
    public String name;
    public int value;

    public CheckboxListViewModel(String name, int value) {
        this.name = name;
        this.value = value;
    }


    public String getName() {
        return this.name;
    }

    public int getValue() {
        return this.value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public void setName(String name) {
        this.name = name;
    }

}
  • How to convert existing layouts (LinearLayout, RelativeLayout, ConstraintLayout) into something Flutter like? (Any tools? Best pratices? Input xml, get dart file to have a base to work on from? )
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/backgroundColor_fragments"
    >

    <ImageView
        android:id="@+id/proceedingCausesAppStateToBeGone_imageView"
        android:src="@drawable/ic_warning"
        android:layout_centerVertical="true"
        android:layout_width="55dp"
        android:layout_height="55dp"
        android:layout_marginLeft="20dp"
        android:layout_marginStart="20dp"        />

    <TextView
        android:id="@+id/proceedingCausesAppStateToBeGone_textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="10dp"
        android:layout_marginRight="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginLeft="10dp"
        android:layout_marginStart="10dp"
        android:gravity="center"
        android:layout_toRightOf="@+id/proceedingCausesAppStateToBeGone_imageView"
        android:layout_toEndOf="@+id/proceedingCausesAppStateToBeGone_imageView"
        android:text="@string/appSettings_exportImport_appState_import_proceedingCausesAppStateToBeGone_DialogText"
        android:textAlignment="center"
        android:textColor="@color/textColor"
        android:textSize="18sp"/>


</RelativeLayout>
  • Complex Activities
package otterfoxxy.motivationsapp.activities;

import com.google.android.material.bottomnavigation.BottomNavigationItemView;
import com.google.android.material.bottomnavigation.BottomNavigationMenuView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.navigation.NavigationView;

import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.widget.Toolbar;

public class AboutScreen extends NavigationDrawerActivity {

    @Override
    protected void onCreate(...) 


    @Override
    protected void onResume() 

    @Override
    public boolean onOptionsItemSelected(MenuItem item) 

    @Override
    public boolean onCreateOptionsMenu(...)

    @Override
    public boolean onKeyUp(...)

    @Override
    public void onStop()
}
package otterfoxxy.motivationsapp.RoomDatabase_Diary;

/**
 * Created by Patrick on 22.01.2018.
 */

import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;

import android.content.Context;

@Database(entities = {Entity_Diary.class, Entity_Tag.class, Entity_Diary_Tag_Relation.class}, version = 9)
public abstract class Database_Diary extends RoomDatabase {

    private static final String DB_NAME = "database_diary.db";
    private static volatile otterfoxxy.motivationsapp.RoomDatabase_Diary.Database_Diary instance;

    static synchronized otterfoxxy.motivationsapp.RoomDatabase_Diary.Database_Diary getInstance(Context context) {
        if (instance == null) {
            instance = create(context);
        }
        return instance;
    }

    private static otterfoxxy.motivationsapp.RoomDatabase_Diary.Database_Diary create(final Context context) {
        return Room.databaseBuilder(
                        context,
                        otterfoxxy.motivationsapp.RoomDatabase_Diary.Database_Diary.class,
                        DB_NAME).allowMainThreadQueries()
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
                .build();
    }

    public abstract DAO_Diary get_DAO_Diary();

    public abstract DAO_Tag get_DAO_Tag();

    public abstract DAO_Diary_Tag_Relation get_DAO_Diary_Tag_Relation();

    private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE Entity_Diary "
                    + " ADD hasImageDiaryEntry INTEGER DEFAULT 0;");

            database.execSQL("ALTER TABLE Entity_Diary "
                    + " ADD imageDiaryEntryFilenameOrUri TEXT DEFAULT \"\" ;");

            database.execSQL("ALTER TABLE Entity_Diary "
                    + " ADD imageDiaryEntryType INTEGER NOT NULL DEFAULT 0;");

        }
    };


    private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

            database.execSQL("CREATE TABLE IF NOT EXISTS `Entity_Tag` (`tagID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `description` TEXT , `iconID` INTEGER  NOT NULL, `position` INTEGER NOT NULL)");
            database.execSQL("CREATE TABLE IF NOT EXISTS `Entity_Diary_Tag_Relation` (`diaryID` INTEGER NOT NULL, `tagID` INTEGER NOT NULL, PRIMARY KEY (diaryID, tagID), FOREIGN KEY(`diaryID`) REFERENCES `Entity_Diary`(`diaryID`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`tagID`) REFERENCES `Entity_Tag`(`tagID`) ON UPDATE NO ACTION ON DELETE CASCADE )");

        }
    };


    private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

        }
    };


    private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE Entity_Diary "
                    + " ADD isFavourite INTEGER DEFAULT 0;");
        }
    };


    private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

            database.execSQL("ALTER TABLE Entity_Diary "
                    + " ADD energy INTEGER NOT NULL DEFAULT -1;");
        }
    };

    /**
     * Invert stress.
     */
    private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

            database.execSQL("UPDATE Entity_Diary "
                    + " SET stresslevel = (10 - stresslevel) WHERE stresslevel != -1;");
        }
    };

    /**
     * Add Tag rating.
     */
    private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

            database.execSQL("ALTER TABLE Entity_Tag "
                    + " ADD rating INTEGER NOT NULL DEFAULT -1;");

            database.execSQL("ALTER TABLE Entity_Tag "
                    + " ADD mood INTEGER NOT NULL DEFAULT -1;");

            database.execSQL("ALTER TABLE Entity_Tag "
                    + " ADD stress INTEGER NOT NULL DEFAULT -1;");

            database.execSQL("ALTER TABLE Entity_Tag "
                    + " ADD energy INTEGER NOT NULL DEFAULT -1;");
        }
    };




    private static final Migration MIGRATION_8_9 = new Migration(8, 9) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {

            database.execSQL("ALTER TABLE Entity_Diary "
                    + " ADD rating INTEGER NOT NULL DEFAULT -1;");
        }
    };
}
package otterfoxxy.motivationsapp.RoomDatabase_Diary;

import static otterfoxxy.motivationsapp.utils.FileUtils.copyFile;
import static otterfoxxy.motivationsapp.utils.FileUtils.getFileExtensionFromPathWithoutLeadingDot;

import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

import androidx.documentfile.provider.DocumentFile;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import otterfoxxy.motivationsapp.R;
import otterfoxxy.motivationsapp.utils.DiaryUtils;
import otterfoxxy.motivationsapp.utils.StringUtils;

/**
 * Created by Patrick on 22.01.2018.
 */

public class DatabaseDiary_Wrapper {
    private Context context;
    private DAO_Diary dao_diary;
    private DAO_Tag dao_tag;
    private DAO_Diary_Tag_Relation dao_diary_tag_relation;

    /*
        constructor
    */
    public DatabaseDiary_Wrapper(Context context) {
        this.context = context;

        dao_diary = Database_Diary
                .getInstance(context)
                .get_DAO_Diary();

        dao_tag = Database_Diary
                .getInstance(context)
                .get_DAO_Tag();

        dao_diary_tag_relation = Database_Diary
                .getInstance(context)
                .get_DAO_Diary_Tag_Relation();
    }




    /**
     * !!POTENTIAL DANGEROUS METHOD!!! Deletes diary entry.
     *
     * @param diaryEntryID
     *         ID of the diary entry to delete.
     */
    public void DELETE_DIARY_ENTRY(long diaryEntryID) {
        dao_diary.DELETE_DIARY_ENTRY(diaryEntryID);
    }



    /**
     * Remove image from diary entry.
     *
     * @param diaryEntryID
     *         ID of the diary entry to update.
     */
    public void removeImageFromDiaryEntry(long diaryEntryID) {
        Entity_Diary entity_diary = getDiaryEntityByID(diaryEntryID);
        entity_diary.hasImageDiaryEntry = false;
        dao_diary.update(entity_diary);
    }


    ...


}
<?xml version="1.0" encoding="utf-8"?>

<!-- Entities to use across this file to avoid redundancy -->
<!DOCTYPE resources [
    <!ENTITY appname "Lebe&#160;Jetzt">
    <!ENTITY menu_about_string "Informationen">
    <!ENTITY aboutScreen_feedback_Button_Text "Feedback / Kontakt">
]>


<resources>

    <!-- Global strings -->

    <string name="app_name">&appname;</string>
    <string name="happyMakingActivities_name">Wohltuendes</string>

    ...


    <string-array name="dailyRating_dialogClosingTexts_badRating">
        <item>Wie wirst Du den Tag beschließen?</item>
        <item>Was könnte diesen Tag noch retten?</item>
        <item>Was machst Du heute Abend?</item>
    </string-array>

    ...
    
</resources>

to

{
  "app_name": "Lebe&#160;Jetzt",
  "happyMakingActivities_name": "Wohltuendes",
   ...
}

Solution

  • As of now - as stated in the comments - it appears that the only option is to manually redevelop the app.

    However, if you went with a model view controller pattern you can use java to dart converter tools to recreate simple classes.

    Although there will be some difficulties you can also convert xml to json using tools.

    The example of the question results in this:

    {
      "string": [
        {
          "@name": "app_name",
          "#text": "Lebe Jetzt"
        },
        {
          "@name": "happyMakingActivities_name",
          "#text": "Wohltuendes"
        }
      ],
      "string-array": {
        "@name": "dailyRating_dialogClosingTexts_badRating",
        "item": [
          "Wie wirst Du den Tag beschließen?",
          "Was könnte diesen Tag noch retten?",
          "Was machst Du heute Abend?"
        ]
      }
    }